<?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: Jesus Oviedo Riquelme</title>
    <description>The latest articles on DEV Community by Jesus Oviedo Riquelme (@jesus_oviedoriquelme_084).</description>
    <link>https://dev.to/jesus_oviedoriquelme_084</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3510064%2Fdf22deb3-2db7-4e2e-8c24-d6ee1de0bcfb.jpg</url>
      <title>DEV Community: Jesus Oviedo Riquelme</title>
      <link>https://dev.to/jesus_oviedoriquelme_084</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jesus_oviedoriquelme_084"/>
    <language>en</language>
    <item>
      <title>LLMZ25-6 Review : Propuesta de Integración con LangChain para lus-laboris-py</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Sun, 26 Oct 2025 18:42:23 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llmz25-6-review-propuesta-de-integracion-con-langchain-para-lus-laboris-py-3b8b</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llmz25-6-review-propuesta-de-integracion-con-langchain-para-lus-laboris-py-3b8b</guid>
      <description>&lt;p&gt;&lt;strong&gt;lus-laboris-py&lt;/strong&gt; es un sistema de investigación legal impulsado por IA que actualmente utiliza una arquitectura basada en FastAPI con integración de OpenAI, Qdrant para búsqueda vectorial, y Arize Phoenix para monitoreo. Este post propone una &lt;strong&gt;modernización arquitectónica&lt;/strong&gt; integrando &lt;strong&gt;LangChain&lt;/strong&gt; para mejorar la modularidad, escalabilidad y mantenibilidad del sistema.&lt;/p&gt;




&lt;h2&gt;
  
  
  Arquitectura Actual vs Propuesta
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Arquitectura Actual de lus-laboris-py
&lt;/h3&gt;

&lt;p&gt;Según el README del proyecto &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;github.com/jesusoviedo/lus-laboris-py&lt;/a&gt;, el sistema actual implementa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usuario → FastAPI → OpenAI LLM → Qdrant Vector Search → Respuesta Legal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stack Tecnológico Actual:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI para la API&lt;/li&gt;
&lt;li&gt;OpenAI para procesamiento de LLM&lt;/li&gt;
&lt;li&gt;Qdrant para búsqueda vectorial&lt;/li&gt;
&lt;li&gt;Arize Phoenix para monitoreo&lt;/li&gt;
&lt;li&gt;Docker para containerización&lt;/li&gt;
&lt;li&gt;UV como gestor de paquetes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Características Actuales:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sistema de investigación legal automatizado&lt;/li&gt;
&lt;li&gt;Integración con Qdrant para búsqueda de documentos legales&lt;/li&gt;
&lt;li&gt;Monitoreo con Phoenix&lt;/li&gt;
&lt;li&gt;Documentación con Guías en docs/README.md&lt;/li&gt;
&lt;li&gt;Deploy a GCP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Limitaciones detectadas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arquitectura sin framework especializado (sin LangChain)&lt;/li&gt;
&lt;li&gt;Flujo de trabajo custom sin estándares LCEL&lt;/li&gt;
&lt;li&gt;Prompts probablemente embebidos en código&lt;/li&gt;
&lt;li&gt;No aprovecha retriever patterns de LangChain&lt;/li&gt;
&lt;li&gt;Limitada modularidad y reutilización&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Arquitectura Propuesta (Con LangChain)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usuario → Query Analysis → Multi-Search → Re-ranking → Legal Analysis → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas de la Propuesta:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modular con &lt;code&gt;RunnableLambda&lt;/code&gt; para orquestación flexible&lt;/li&gt;
&lt;li&gt;Prompts separados con &lt;code&gt;PromptTemplate&lt;/code&gt; para mejor mantenibilidad&lt;/li&gt;
&lt;li&gt;Recuperación avanzada con &lt;code&gt;ParentDocumentRetriever&lt;/code&gt; para jerarquía de documentos&lt;/li&gt;
&lt;li&gt;Re-ranking con &lt;code&gt;ContextualCompressionRetriever&lt;/code&gt; y Flashrank&lt;/li&gt;
&lt;li&gt;Observabilidad mejorada con Phoenix integrado a LangChain&lt;/li&gt;
&lt;li&gt;Escalable con LCEL (LangChain Expression Language)&lt;/li&gt;
&lt;li&gt;Patterns estándar de la industria&lt;/li&gt;
&lt;li&gt;Fácil integración con nuevos LLMs (Claude, Gemini, etc.)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implementación Propuesta
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Estructura de Prompts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain.prompts import PromptTemplate

QUERY_ANALYSIS_PROMPT = PromptTemplate(
    input_variables=["user_query", "country", "legal_area"],
    template="""
Eres un experto en análisis de consultas legales.

Consulta del usuario: {user_query}
País: {country}
Área legal: {legal_area}

Instrucciones:
1. Identifica el tipo de consulta legal
2. Extrae conceptos clave y términos legales relevantes
3. Genera entre 3-5 consultas de búsqueda optimizadas

Devuelve JSON con: query_type, key_concepts, search_queries, related_areas
"""
)

LEGAL_ANALYSIS_PROMPT = PromptTemplate(
    input_variables=["query", "legal_documents", "jurisdiction"],
    template="""
Eres un abogado experto analizando documentos legales.

Consulta: {query}
Jurisdicción: {jurisdiction}

Documentos legales:
{legal_documents}

Analiza y proporciona: resumen ejecutivo, análisis legal, leyes relevantes, recomendaciones
"""
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Pipeline con RunnableLambda
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_core.runnables import RunnableLambda
from langchain_openai import ChatOpenAI
from langchain_qdrant import QdrantVectorStore

llm = ChatOpenAI(model="gpt-4o", temperature=0.3)

def _analyze_query(inputs):
    """Etapa 1: Analizar consulta"""
    chain = QUERY_ANALYSIS_PROMPT | llm
    result = chain.invoke({
        "user_query": inputs["query"],
        "country": inputs["country"],
        "legal_area": inputs["legal_area"]
    })
    return {**inputs, "query_analysis": parse_json(result.content)}

def _retrieve_docs(inputs):
    """Etapa 2: Recuperar documentos"""
    all_docs = []
    for query in inputs["search_queries"]:
        docs = compressed_retriever.invoke(query)
        all_docs.extend(docs)
    return {**inputs, "documents": deduplicate(all_docs)}

def _analyze_legal(inputs):
    """Etapa 3: Análisis legal con LLM"""
    chain = LEGAL_ANALYSIS_PROMPT | llm
    result = chain.invoke({
        "query": inputs["query"],
        "legal_documents": format_docs(inputs["documents"]),
        "jurisdiction": inputs["country"]
    })
    return {**inputs, "legal_analysis": result.content}

# Pipeline completo
legal_workflow = (
    RunnableLambda(_analyze_query)
    | RunnableLambda(_retrieve_docs)
    | RunnableLambda(_analyze_legal)
)

def process_legal_query(query: str, country: str = "Spain"):
    """Procesar consulta legal"""
    return legal_workflow.invoke({"query": query, "country": country})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Comparación Antes vs Después
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Sin LangChain&lt;/th&gt;
&lt;th&gt;Con LangChain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Embebidos en código&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PromptTemplate&lt;/code&gt; separado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workflow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcodeado&lt;/td&gt;
&lt;td&gt;Pipeline modular&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retrieval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ParentDocumentRetriever&lt;/code&gt; + re-ranking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observabilidad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limitada&lt;/td&gt;
&lt;td&gt;Phoenix integrado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Difícil&lt;/td&gt;
&lt;td&gt;Traces completos&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Ventajas de la Nueva Arquitectura
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;✅ &lt;strong&gt;Modularidad&lt;/strong&gt;: Componentes intercambiables&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Observabilidad&lt;/strong&gt;: Traces con Phoenix&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Escalabilidad&lt;/strong&gt;: Fácil agregar features&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Mantenibilidad&lt;/strong&gt;: Código más limpio&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Optimización&lt;/strong&gt;: A/B testing de prompts&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Performance&lt;/strong&gt;: Retrieval avanzado + re-ranking&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Plan de Migración (10 semanas)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fase 1-2: Preparación (Semanas 1-2)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Instalar dependencias de LangChain: &lt;code&gt;uv add langchain langchain-openai langchain-qdrant&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Configurar integración de Phoenix con LangChain&lt;/li&gt;
&lt;li&gt;Revisar estructura actual del proyecto&lt;/li&gt;
&lt;li&gt;Estudiar README y documentación existente&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fase 3-4: Migración de Prompts (Semanas 3-4)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Extraer prompts del código actual&lt;/li&gt;
&lt;li&gt;Convertir a &lt;code&gt;PromptTemplate&lt;/code&gt; de LangChain&lt;/li&gt;
&lt;li&gt;Crear archivo &lt;code&gt;legal_prompts.py&lt;/code&gt; con templates especializados&lt;/li&gt;
&lt;li&gt;Testing A/B de prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fase 5-6: Implementación de Retrievers (Semanas 5-6)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Configurar &lt;code&gt;ParentDocumentRetriever&lt;/code&gt; con Qdrant existente&lt;/li&gt;
&lt;li&gt;Implementar &lt;code&gt;ContextualCompressionRetriever&lt;/code&gt; con Flashrank&lt;/li&gt;
&lt;li&gt;Optimizar chunking para documentos legales (500-800 chars)&lt;/li&gt;
&lt;li&gt;Implementar deduplicación de resultados&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fase 7-8: Pipeline con RunnableLambda (Semanas 7-8)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Convertir funciones existentes a &lt;code&gt;RunnableLambda&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Implementar &lt;code&gt;workflow_chain&lt;/code&gt; con LCEL&lt;/li&gt;
&lt;li&gt;Migrar lógica de búsqueda a retriever chain&lt;/li&gt;
&lt;li&gt;Testing end-to-end&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fase 9-10: Optimización y Deployment (Semanas 9-10)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Configurar observabilidad mejorada con Phoenix&lt;/li&gt;
&lt;li&gt;Monitorear métricas (latency, cost, quality)&lt;/li&gt;
&lt;li&gt;Optimizar prompts y parámetros&lt;/li&gt;
&lt;li&gt;Actualizar documentación y guías&lt;/li&gt;
&lt;li&gt;Deploy a GCP con nueva arquitectura&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;La integración de &lt;strong&gt;LangChain&lt;/strong&gt; en &lt;strong&gt;lus-laboris-py&lt;/strong&gt; transformará el sistema en una arquitectura &lt;strong&gt;moderna, observable y escalable&lt;/strong&gt;, siguiendo las mejores prácticas demostradas en el proyecto &lt;a href="https://github.com/clementlwm94/ziweidoushu" rel="noopener noreferrer"&gt;ziweidoushu&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beneficios Esperados
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Modularidad Mejorada&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Componentes intercambiables (Qdrant, Pinecone, etc.)&lt;/li&gt;
&lt;li&gt;Fácil cambiar de GPT-4 a Claude u otros LLMs&lt;/li&gt;
&lt;li&gt;Prompts reutilizables y versionados&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Observabilidad Profesional&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traces automáticos con Phoenix&lt;/li&gt;
&lt;li&gt;Métricas detalladas de latency, tokens, costos&lt;/li&gt;
&lt;li&gt;Debug simplificado con spans y context&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Escalabilidad Futura&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agregar nuevas fuentes legales es trivial&lt;/li&gt;
&lt;li&gt;Soporte multi-idioma con translate chains&lt;/li&gt;
&lt;li&gt;Agentes para búsqueda dinámica&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mantenibilidad&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Código más limpio y organizado&lt;/li&gt;
&lt;li&gt;Separación clara de concerns&lt;/li&gt;
&lt;li&gt;Testing más fácil de unit e integration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Repositorios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ziweidoushu&lt;/strong&gt; (referencia): &lt;a href="https://github.com/clementlwm94/ziweidoushu" rel="noopener noreferrer"&gt;github.com/clementlwm94/ziweidoushu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lus-laboris-py&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;github.com/jesusoviedo/lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Referencias:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/" rel="noopener noreferrer"&gt;LangChain Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/docs/expression_language/" rel="noopener noreferrer"&gt;RunnableLambda&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/docs/modules/data_connection/retrievers/parent_document_retriever" rel="noopener noreferrer"&gt;ParentDocumentRetriever&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/docs/integrations/vectorstores/qdrant" rel="noopener noreferrer"&gt;Qdrant Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.arize.com/phoenix" rel="noopener noreferrer"&gt;Arize Phoenix&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>LLMZ25-5 Review : Arquitectura RAG del Proyecto Ziweidoushu</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Sun, 26 Oct 2025 18:33:42 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llmz25-5-review-arquitectura-rag-del-proyecto-ziweidoushu-5cck</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llmz25-5-review-arquitectura-rag-del-proyecto-ziweidoushu-5cck</guid>
      <description>&lt;p&gt;En este post, exploramos cómo &lt;strong&gt;LangChain&lt;/strong&gt; se integra en un sistema RAG (Retrieval-Augmented Generation) real para crear un asistente de astrología china (紫微斗数). El proyecto &lt;strong&gt;ziweidoushu&lt;/strong&gt; es un ejemplo de arquitectura de producción que utiliza LangChain para orquestar un flujo complejo de procesamiento de documentos, recuperación híbrida, y generación de respuestas personalizadas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repositorio del Proyecto:&lt;/strong&gt; &lt;a href="https://github.com/clementlwm94/ziweidoushu" rel="noopener noreferrer"&gt;github.com/clementlwm94/ziweidoushu&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ziweidoushu&lt;/strong&gt; es un sistema que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Genera cartas natales chinas desde datos de nacimiento&lt;/li&gt;
&lt;li&gt;Recupera conocimiento relevante de una base de datos vectorial (Qdrant)&lt;/li&gt;
&lt;li&gt;Combina información específica del usuario con conocimiento del dominio&lt;/li&gt;
&lt;li&gt;Genera respuestas personalizadas usando LLMs (GPT-4)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  ¿Qué es LangChain y por qué usarlo?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LangChain&lt;/strong&gt; es un framework de Python diseñado para construir aplicaciones basadas en LLMs (Large Language Models). Proporciona:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chains (Cadenas)&lt;/strong&gt;: Flujos de trabajo pre-construidos que conectan múltiples componentes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents (Agentes)&lt;/strong&gt;: Componentes que pueden tomar decisiones y usar herramientas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory (Memoria)&lt;/strong&gt;: Gestión de estado conversacional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: Plantillas reutilizables para interacciones con LLMs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrievers&lt;/strong&gt;: Integraciones con bases de datos vectoriales y sistemas de búsqueda&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas de usar LangChain:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Modularidad: Componentes intercambiables&lt;/li&gt;
&lt;li&gt;✅ Escalabilidad: Fácil agregar nuevas funcionalidades&lt;/li&gt;
&lt;li&gt;✅ Observabilidad: Integración nativa con Phoenix/OpenTelemetry&lt;/li&gt;
&lt;li&gt;✅ Estándares: Protocolo LCEL (LangChain Expression Language)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Arquitectura del Proyecto Ziweidoushu
&lt;/h2&gt;

&lt;p&gt;El flujo de trabajo de &lt;strong&gt;ziweidoushu&lt;/strong&gt; implementa un &lt;strong&gt;pipeline RAG de 9 etapas&lt;/strong&gt; usando LangChain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usuario → Generación de Carta → Generación de Consultas → Recuperación → Resumen → Generación de Respuesta → Respuesta Final
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Componentes LangChain Utilizados
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. PromptTemplate: Gestión de Prompts Especializados
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;LangChain &lt;code&gt;PromptTemplate&lt;/code&gt;&lt;/strong&gt; permite crear prompts reutilizables con variables dinámicas. En el proyecto, se definen múltiples prompts especializados:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain.prompts import PromptTemplate

query_prompt_text = """
角色: 你是一个专业的紫微斗数检索查询生成器

目标: 基于用户的命盘关键信息与具体问题，生成若干高质量、可直接用于检索的查询字符串

约束:
- 仅使用用户的命盘中出现的宫位、主星、辅星、煞星；不要编造
- 使用标准中文术语与命名
- 生成 1-3 条查询，按相关性从高到低排序

输入:
- 用户命盘: {user_chart}
- 用户问题: {user_question}

输出: 仅输出 JSON 字符串数组
"""

query_prompt = PromptTemplate(
    input_variables=["user_question", "user_chart"],
    template=query_prompt_text,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separación de lógica y prompts&lt;/li&gt;
&lt;li&gt;Reutilización en múltiples contextos&lt;/li&gt;
&lt;li&gt;Fácil mantenimiento y actualización&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. RunnableLambda: Orquestación del Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;RunnableLambda&lt;/code&gt;&lt;/strong&gt; permite encadenar funciones Python en un flujo de trabajo secuencial. El proyecto implementa un pipeline de 9 etapas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_core.runnables import RunnableLambda

def _with_chart(inputs: Dict[str, Any]) -&amp;gt; Dict[str, Any]:
    """Etapa 1: Genera la carta natal"""
    chart = full_chart_generation(inputs["birth_date"], inputs["birth_hour"], inputs["gender"])
    return {**inputs, "user_chart": chart}

def _with_queries(inputs: Dict[str, Any]) -&amp;gt; Dict[str, Any]:
    """Etapa 4: Genera consultas de búsqueda"""
    queries = generate_queries(
        inputs["translated_question"],
        inputs["user_chart"],
        top_n=inputs.get("top_n_queries", 3),
        llm=inputs.get("llm"),
    )
    return {**inputs, "queries": queries}

# Pipeline completo con RunnableLambda
workflow_chain = (
    RunnableLambda(_with_chart)          # 1. Generar carta
    | RunnableLambda(_with_llm)          # 2. Inicializar LLM
    | RunnableLambda(_with_translated_question)  # 3. Traducir pregunta
    | RunnableLambda(_with_queries)      # 4. Generar consultas
    | RunnableLambda(_with_documents)     # 5. Recuperar documentos
    | RunnableLambda(_with_documents_text) # 6. Formatear documentos
    | RunnableLambda(_with_summary)       # 7. Resumir pasajes
    | RunnableLambda(_with_answer)         # 8. Generar respuesta
    | RunnableLambda(_with_localized_answer) # 9. Localizar respuesta
    | RunnableLambda(_finalize)           # Finalizar
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Por qué &lt;code&gt;RunnableLambda&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expresa flujo secuencial claramente&lt;/li&gt;
&lt;li&gt;Permite paso de estado entre etapas&lt;/li&gt;
&lt;li&gt;Integración con herramientas de observabilidad (Phoenix)&lt;/li&gt;
&lt;li&gt;Composición flexible con operador &lt;code&gt;|&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. ParentDocumentRetriever: Recuperación Jerárquica
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ParentDocumentRetriever&lt;/code&gt;&lt;/strong&gt; implementa un patrón de recuperación &lt;strong&gt;jerárquico&lt;/strong&gt; que combina chunks pequeños (para búsqueda precisa) con documentos padres completos (para contexto):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import LocalFileStore
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Almacén de documentos padres
fs = LocalFileStore(STORE_PATH)
doc_store = create_kv_docstore(fs)

# Splitter para chunks hijos
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)

# Retriever jerárquico
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,           # Busca chunks pequeños
    docstore=doc_store,                 # Trae documentos padres completos
    child_splitter=child_splitter,
    search_kwargs={"k": 10},           # Top-10 chunks
    id_key="source_id"
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Cómo funciona?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Busca en vector DB usando chunks pequeños (400 chars)&lt;/li&gt;
&lt;li&gt;Recupera documentos padres completos desde docstore&lt;/li&gt;
&lt;li&gt;Proporciona contexto más rico que solo chunks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Precisión de búsqueda (chunks pequeños)&lt;/li&gt;
&lt;li&gt;✅ Contexto completo (documentos padres)&lt;/li&gt;
&lt;li&gt;✅ Mejor calidad de RAG&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. ContextualCompressionRetriever: Re-ranking con Flashrank
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ContextualCompressionRetriever&lt;/code&gt;&lt;/strong&gt; mejora la calidad de documentos recuperados mediante &lt;strong&gt;re-ranking contextual&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;from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
from langchain_community.document_compressors import FlashrankRerank

# Compresor de contexto
compressor = FlashrankRerank()

# Retriever con compresión
compression_retriever = ContextualCompressionRetriever(
    base_retriever=retriever,
    base_compressor=compressor,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Qué hace FlashrankRerank?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usa un modelo de re-ranking (MRM3) entrenado para relevancia&lt;/li&gt;
&lt;li&gt;Reordena documentos por relevancia contextual&lt;/li&gt;
&lt;li&gt;Mejora la precisión del Top-K&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Reduce ruido de recuperación inicial&lt;/li&gt;
&lt;li&gt;✅ Mejora precisión en Top-5/Top-10&lt;/li&gt;
&lt;li&gt;✅ Mejor calidad de contexto para LLM&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5. ChatOpenAI: Integración con LLMs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ChatOpenAI&lt;/code&gt;&lt;/strong&gt; proporciona una interfaz unificada para interactuar con modelos OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_openai import ChatOpenAI

def _create_llm(api_key: Optional[str], llm_model: Optional[str]) -&amp;gt; ChatOpenAI:
    target_model = llm_model or LLM_MODEL  # "gpt-4o-mini"
    return ChatOpenAI(
        model=target_model,
        temperature=LLM_TEMPERATURE,  # 0.3
        api_key=api_key
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API unificada (no importa si usas GPT-4, Claude, etc.)&lt;/li&gt;
&lt;li&gt;Integración con LCEL (LangChain Expression Language)&lt;/li&gt;
&lt;li&gt;Manejo automático de rate limiting&lt;/li&gt;
&lt;li&gt;Streaming support&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  6. QdrantVectorStore: Búsqueda Híbrida
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;QdrantVectorStore&lt;/code&gt;&lt;/strong&gt; integra Qdrant con LangChain para &lt;strong&gt;búsqueda híbrida&lt;/strong&gt; (dense + sparse):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_qdrant import QdrantVectorStore, FastEmbedSparse, RetrievalMode
from langchain_community.embeddings.fastembed import FastEmbedEmbeddings

# Embeddings densos (semánticos)
embeddings = FastEmbedEmbeddings(model_name="jinaai/jina-embeddings-v2-base-zh")

# Embeddings sparse (keyword-based)
sparse_embeddings = FastEmbedSparse(model_name="Qdrant/BM25")

# Vector store con modo híbrido
vectorstore = QdrantVectorStore(
    embedding=embeddings,                 # Dense embeddings
    client=client,
    collection_name=COLLECTION_NAME,
    sparse_embedding=sparse_embeddings,   # Sparse embeddings
    retrieval_mode=RetrievalMode.HYBRID,  # 🎯 Modo híbrido
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Qué es búsqueda híbrida?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dense&lt;/strong&gt;: Captura similitud semántica&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparse (BM25)&lt;/strong&gt;: Captura coincidencias de keywords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Híbrida&lt;/strong&gt;: Combina ambos para mejor recall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Mejor recall que solo dense o solo sparse&lt;/li&gt;
&lt;li&gt;✅ Optimizado para preguntas específicas de dominio&lt;/li&gt;
&lt;li&gt;✅ Embeddings optimizados para chino (jina-v2-base-zh)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Flujo de Trabajo Completo
&lt;/h2&gt;

&lt;p&gt;El pipeline completo de &lt;strong&gt;ziweidoushu&lt;/strong&gt; con LangChain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    A[User Question + Birth Data] --&amp;gt; B[Generate Chart]
    B --&amp;gt; C[Translate Question]
    C --&amp;gt; D[Generate Queries]
    D --&amp;gt; E[Hybrid Retrieval]
    E --&amp;gt; F[Flashrank Rerank]
    F --&amp;gt; G[Summarize Passages]
    G --&amp;gt; H[Generate Answer]
    H --&amp;gt; I[Translate Answer]
    I --&amp;gt; J[Final Response]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Implementación con LangChain:&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;def run_qdrant_rag_workflow(
    question: str,
    birth_date: str,
    birth_hour: int,
    gender: str,
    top_n_queries: int = 5,
    api_key: Optional[str] = None,
    llm_model: Optional[str] = None,
) -&amp;gt; Dict[str, Any]:
    """Ejecutar el workflow RAG completo de Qdrant."""

    # Crear payload inicial
    payload = {
        "question": question,
        "birth_date": birth_date,
        "birth_hour": birth_hour,
        "gender": gender,
        "top_n_queries": top_n_queries,
        "api_key": api_key,
        "llm_model": llm_model,
    }

    # Ejecutar pipeline completo
    result = workflow_chain.invoke(payload)

    return result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Ventajas de Usar LangChain
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Modularidad Extrema&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada componente es intercambiable&lt;/li&gt;
&lt;li&gt;Fácil cambiar de Qdrant a Pinecone&lt;/li&gt;
&lt;li&gt;Fácil cambiar de GPT-4 a Claude&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Observabilidad Integrada&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traces automáticos con Phoenix&lt;/li&gt;
&lt;li&gt;Mide latencia, tokens, costos&lt;/li&gt;
&lt;li&gt;Debug más fácil con spans detallados&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LCEL (LangChain Expression Language)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composición fluida con &lt;code&gt;|&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Streaming nativo&lt;/li&gt;
&lt;li&gt;Batching automático&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retrievers Reutilizables&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ParentDocumentRetriever&lt;/code&gt; con docstore&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ContextualCompressionRetriever&lt;/code&gt; con re-ranking&lt;/li&gt;
&lt;li&gt;Fácil cambiar estrategias de recuperación&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ecosistema Maduro&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integración con 100+ herramientas&lt;/li&gt;
&lt;li&gt;Documentación excelente&lt;/li&gt;
&lt;li&gt;Comunidad activa&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Lecciones Aprendidas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Usa &lt;code&gt;RunnableLambda&lt;/code&gt; para pipelines secuenciales complejos&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Más flexibilidad que &lt;code&gt;SequentialChain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Permite paso de estado complejo&lt;/li&gt;
&lt;li&gt;Integración mejor con observabilidad&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separa prompts del código&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PromptTemplate&lt;/code&gt; hace código más limpio&lt;/li&gt;
&lt;li&gt;Fácil iterar en prompts sin tocar lógica&lt;/li&gt;
&lt;li&gt;Reutilización en múltiples contextos&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Invierte en retrieval quality&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ParentDocumentRetriever&lt;/code&gt; + &lt;code&gt;ContextualCompressionRetriever&lt;/code&gt; = mejor RAG&lt;/li&gt;
&lt;li&gt;Búsqueda híbrida mejora recall&lt;/li&gt;
&lt;li&gt;Re-ranking mejora precisión&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Monitorea todo&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integra Phoenix desde el inicio&lt;/li&gt;
&lt;li&gt;Mide métricas clave (latency, quality)&lt;/li&gt;
&lt;li&gt;Debug con traces&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Optimiza para tu dominio&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings específicos (jina-v2-base-zh para chino)&lt;/li&gt;
&lt;li&gt;Prompts especializados por tarea&lt;/li&gt;
&lt;li&gt;Chunking adaptado a tu contenido&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;El proyecto &lt;strong&gt;ziweidoushu&lt;/strong&gt; demuestra cómo LangChain puede usarse para construir un sistema RAG de producción robusto y escalable. Los componentes clave que hacen esto posible son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RunnableLambda&lt;/strong&gt;: Orquestación flexible del pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PromptTemplate&lt;/strong&gt;: Gestión de prompts especializados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ParentDocumentRetriever&lt;/strong&gt;: Recuperación jerárquica&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ContextualCompressionRetriever&lt;/strong&gt;: Re-ranking contextual&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QdrantVectorStore&lt;/strong&gt;: Búsqueda híbrida&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatOpenAI&lt;/strong&gt;: Integración LLM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La arquitectura resultante es &lt;strong&gt;modular, observable y mantenible&lt;/strong&gt;, lo que permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Iteración rápida en prompts&lt;/li&gt;
&lt;li&gt;Cambio fácil de componentes&lt;/li&gt;
&lt;li&gt;Debug eficiente con Phoenix&lt;/li&gt;
&lt;li&gt;Escalabilidad hacia más funcionalidades&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/" rel="noopener noreferrer"&gt;LangChain Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.arize.com/phoenix" rel="noopener noreferrer"&gt;Arize Phoenix&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>LLMZ25-4 Review : Mejorando React + TailwindCSS</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Sun, 26 Oct 2025 14:27:48 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llmz25-4-review-mejorando-react-tailwindcss-2mc9</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llmz25-4-review-mejorando-react-tailwindcss-2mc9</guid>
      <description>&lt;p&gt;Siguiendo nuestra discusión previa sobre React y TailwindCSS en &lt;a href="https://github.com/mavcr/quizmate" rel="noopener noreferrer"&gt;QuizMate&lt;/a&gt;, este post explora &lt;strong&gt;cómo mejorar&lt;/strong&gt; las implementaciones de estas tecnologías, ya sea que estés empezando desde el enfoque de QuizMate o integrándolas en proyectos como &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cubriremos mejoras listas para producción, optimizaciones de rendimiento y patrones profesionales que elevan tus aplicaciones React + TailwindCSS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Análisis del Estado Actual: QuizMate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lo que QuizMate Hace Bien ✅
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configuración simple&lt;/strong&gt; - Enfoque basado en CDN elimina la complejidad del build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arquitectura basada en componentes&lt;/strong&gt; - Separación limpia de responsabilidades&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clases de utilidad de TailwindCSS&lt;/strong&gt; - Estilizado rápido sin CSS personalizado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manejo de estado reactivo&lt;/strong&gt; - useEffect y useState para flujo de datos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buenos patrones de UX&lt;/strong&gt; - Estados de carga, manejo de errores, renderizado condicional&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Áreas para Mejorar 📈
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Optimizaciones de rendimiento&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organización de código y reutilización&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proceso de build para producción&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seguridad de tipos y límites de errores&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estrategias de testing&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Mejora 1: Migrar a una Configuración con Build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enfoque Actual (CDN)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Simple pero limitado --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/react@18/umd/react.production.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enfoque Mejorado (Con Build)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Paso 1: Inicializar un Proyecto React Moderno&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;# Usando Vite (Recomendado - Rápido y Moderno)&lt;/span&gt;
npm create vite@latest quizmate-frontend &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; react

&lt;span class="c"&gt;# O usando Create React App&lt;/span&gt;
npx create-react-app quizmate-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Paso 2: Instalar TailwindCSS&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="nb"&gt;cd &lt;/span&gt;quizmate-frontend
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss postcss autoprefixer
npx tailwindcss init &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Paso 3: Configurar TailwindCSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/**/*.{js,ts,jsx,tsx}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quiz-blue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#3B82F6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quiz-green&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#10B981&lt;/span&gt;&lt;span class="dl"&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;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&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;strong&gt;Paso 4: Agregar Directivas de TailwindCSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/index.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.btn-primary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;bg-blue-600&lt;/span&gt; &lt;span class="err"&gt;text-white&lt;/span&gt; &lt;span class="err"&gt;px-4&lt;/span&gt; &lt;span class="err"&gt;py-2&lt;/span&gt; &lt;span class="err"&gt;rounded-md&lt;/span&gt; 
           &lt;span class="err"&gt;font-medium&lt;/span&gt; &lt;span class="py"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;bg-blue-700&lt;/span&gt; &lt;span class="n"&gt;transition-colors&lt;/span&gt; 
           &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;bg-gray-300&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;cursor-not-allowed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;bg-white&lt;/span&gt; &lt;span class="err"&gt;rounded-lg&lt;/span&gt; &lt;span class="err"&gt;shadow-md&lt;/span&gt; &lt;span class="err"&gt;p-6;&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;strong&gt;Beneficios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Tree-shaking elimina CSS no usado&lt;/li&gt;
&lt;li&gt;✅ Carga de páginas más rápida&lt;/li&gt;
&lt;li&gt;✅ Mejor experiencia de desarrollo (hot reload)&lt;/li&gt;
&lt;li&gt;✅ Puedes usar TypeScript&lt;/li&gt;
&lt;li&gt;✅ Tamaños de bundle más pequeños&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Mejora 2: Extracción de Componentes y Reutilización
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Código Actual (Todo en un Archivo)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 500+ líneas de código en un archivo&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Estructura Mejorada
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── components/
│   ├── common/
│   │   ├── Button.jsx
│   │   ├── Card.jsx
│   │   ├── Input.jsx
│   │   └── LoadingSpinner.jsx
│   ├── quiz/
│   │   ├── FileUpload.jsx
│   │   ├── QuizCreator.jsx
│   │   └── QuizTaker.jsx
│   └── layout/
│       ├── Header.jsx
│       └── Footer.jsx
├── hooks/
│   ├── useQuizGeneration.js
│   ├── useFileUpload.js
│   └── useSources.js
├── services/
│   └── api.js
└── utils/
    └── validation.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ejemplo: Componente Botón Reutilizable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/components/common/Button.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prop-types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; 
    &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; 
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clasesBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;font-medium rounded-md transition-colors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clasesVariante&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-blue-600 text-white hover:bg-blue-700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-gray-200 text-gray-900 hover:bg-gray-300&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-green-600 text-white hover:bg-green-700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;danger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-red-600 text-white hover:bg-red-700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clasesTamaño&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px-3 py-1.5 text-sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px-4 py-2 text-base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px-6 py-3 text-lg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clasesDeshabilitado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled:bg-gray-300 disabled:cursor-not-allowed&lt;/span&gt;&lt;span class="dl"&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
            &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clasesBase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clasesVariante&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; 
                       &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clasesTamaño&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;clasesDeshabilitado&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;propTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isRequired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secondary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;string&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;strong&gt;Uso:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/common/Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; 
    &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"md"&lt;/span&gt;
    &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Generar Quiz
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mejora 3: Hooks Personalizados para Separación de Lógica
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Código Actual (Lógica Mezclada con UI)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;QuizCreator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSources&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;generating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGenerating&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Lógica de API mezclada con UI&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchSources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mejorado: Extraer a Hook Personalizado
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/hooks/useQuizGeneration.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useQuizGeneration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSources&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;fetchSources&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchSources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ingestion/sources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Falló al obtener fuentes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;setSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateQuiz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Falló al generar quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;generateQuiz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;refetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchSources&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;strong&gt;Uso:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuizGeneration&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./hooks/useQuizGeneration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;QuizCreator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generateQuiz&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuizGeneration&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleGenerate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateQuiz&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="na"&gt;numberOfQuestions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="nf"&gt;onQuizGenerated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Falló al generar quiz:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Componentes UI */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;strong&gt;Beneficios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Lógica reutilizable&lt;/li&gt;
&lt;li&gt;✅ Testeable de forma aislada&lt;/li&gt;
&lt;li&gt;✅ Código de componente más limpio&lt;/li&gt;
&lt;li&gt;✅ Mejor manejo de errores&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Mejora 4: Capa de Servicio API
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Código Actual (Llamadas Fetch Inline)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mejorado: Servicio API Centralizado
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/services/api.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error API [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&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;async&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;additionalData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&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;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;additionalData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;additionalData&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="c1"&gt;// Dejar que el navegador establezca Content-Type con boundary&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiService&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;ApiService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Métodos API específicos&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quizApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;apiService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;apiService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/evaluation/evaluate-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ingestionApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;apiService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ingestion/upload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sourceName&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;getSources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;apiService&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ingestion/sources&lt;/span&gt;&lt;span class="dl"&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;strong&gt;Uso:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;quizApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ingestionApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./services/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Limpio y simple&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;quizApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;numberOfQuestions&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ingestionApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSources&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ingestionApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sourceName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mejora 5: Mejores Prácticas de TailwindCSS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Usar &lt;a class="mentioned-user" href="https://dev.to/apply"&gt;@apply&lt;/a&gt; para Patrones Repetitivos
&lt;/h3&gt;

&lt;p&gt;En lugar de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-white rounded-lg shadow-md p-6"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crear componentes reutilizables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* tailwind.css */&lt;/span&gt;
&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;bg-white&lt;/span&gt; &lt;span class="err"&gt;rounded-lg&lt;/span&gt; &lt;span class="err"&gt;shadow-md&lt;/span&gt; &lt;span class="err"&gt;p-6;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.card-hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;card&lt;/span&gt; &lt;span class="py"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;shadow-lg&lt;/span&gt; &lt;span class="n"&gt;transition-shadow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.input-field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;@apply&lt;/span&gt; &lt;span class="err"&gt;block&lt;/span&gt; &lt;span class="err"&gt;w-full&lt;/span&gt; &lt;span class="err"&gt;px-3&lt;/span&gt; &lt;span class="err"&gt;py-2&lt;/span&gt; &lt;span class="err"&gt;border&lt;/span&gt; &lt;span class="err"&gt;border-gray-300&lt;/span&gt; 
           &lt;span class="err"&gt;rounded-md&lt;/span&gt; &lt;span class="err"&gt;shadow-sm&lt;/span&gt; &lt;span class="py"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;outline-none&lt;/span&gt; 
           &lt;span class="n"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ring-blue-500&lt;/span&gt; &lt;span class="n"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;border-blue-500&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;Uso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card-hover"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"input-field"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Usar Variables CSS para Temas
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;var(--color-primary-50)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;var(--color-primary-500)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;var(--color-primary-900)&lt;/span&gt;&lt;span class="dl"&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;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;h3&gt;
  
  
  3. Organizar con Valores Arbitrarios
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tamaños de texto responsivos&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-[clamp(1rem,4vw,2rem)]"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Texto Adaptativo
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Cálculos complejos&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"w-[calc(100%-2rem)]"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Ancho Calculado
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Mejor que hardcoded&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-[600px]"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Altura Fija
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Usar Modo JIT
&lt;/h3&gt;

&lt;p&gt;Asegurar que JIT está habilitado en &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Compilación Just-in-Time&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/**/*.{js,jsx,ts,tsx}&lt;/span&gt;&lt;span class="dl"&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;h2&gt;
  
  
  Mejora 6: Límites de Errores y Estados de Carga
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Componente Error Boundary
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/components/common/ErrorBoundary.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;getDerivedStateFromError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&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="na"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;componentDidCatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errorInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error capturado por boundary:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errorInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasError&lt;/span&gt;&lt;span class="p"&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen flex items-center justify-center bg-gray-50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold text-red-600 mb-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                            Algo salió mal
                        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600 mb-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ocurrió un error inesperado&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
                            &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&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;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"&lt;/span&gt;
                        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                            Recargar Página
                        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;ErrorBoundary&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;Uso:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/common/ErrorBoundary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TusComponentes&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt;&lt;span class="p"&gt;&amp;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;h3&gt;
  
  
  Componente de Carga
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/components/common/LoadingSpinner.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sizeClasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w-4 h-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w-8 h-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w-12 h-12&lt;/span&gt;&lt;span class="dl"&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`flex justify-center items-center &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sizeClasses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;h2&gt;
  
  
  Mejora 7: Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configurar Testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; vitest @testing-library/react @testing-library/jest-dom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;src/__tests__/Button.test.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fireEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../components/common/Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Componente Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renderiza con texto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Haz Clic&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Haz Clic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llama onClick cuando se hace clic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Clic&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;fireEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Clic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledTimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;está deshabilitado cuando disabled prop es true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Clic&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Clic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeDisabled&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;h2&gt;
  
  
  Aplicar Estas Mejoras a lus-laboris-py
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Camino de Migración
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fase 1: Configurar Build Moderno&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;npm create vite@latest lus-laboris-frontend &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; react
&lt;span class="nb"&gt;cd &lt;/span&gt;lus-laboris-frontend
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; tailwindcss postcss autoprefixer
npx tailwindcss init &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fase 2: Crear Integración API&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;npm &lt;span class="nb"&gt;install &lt;/span&gt;axios &lt;span class="c"&gt;# o usar fetch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fase 3: Estructura de Componentes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api/lusLaborisApi.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:5000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Tu backend Python&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lusLaborisApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;getJobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/trabajos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;getJobById&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&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="s2"&gt;`/api/trabajos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;aplicarTrabajo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;datosAplicacion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/trabajos/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/aplicar`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;datosAplicacion&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;strong&gt;Fase 4: Migración Gradual&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Comenzar con nuevas características en React&lt;/li&gt;
&lt;li&gt;Mantener funcionalidad existente funcionando&lt;/li&gt;
&lt;li&gt;Migrar página por página&lt;/li&gt;
&lt;li&gt;Mantener compatibilidad de API&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Despliegue en Producción
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build para Producción
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;span class="c"&gt;# Salida: dist/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Servir con tu Backend
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Python (Flask):&lt;/strong&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;send_from_directory&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;static_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;../lus-laboris-frontend/dist&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&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;send_from_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;../lus-laboris-frontend/dist&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;index.html&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;&lt;strong&gt;Python (FastAPI):&lt;/strong&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.staticfiles&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StaticFiles&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FileResponse&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StaticFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../lus-laboris-frontend/dist&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;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;FileResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;../lus-laboris-frontend/dist/index.html&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;h2&gt;
  
  
  Puntos Clave
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configuración con build&lt;/strong&gt; proporciona mejor DX y rendimiento&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extracción de componentes&lt;/strong&gt; mejora mantenibilidad&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks personalizados&lt;/strong&gt; separan lógica de presentación&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capa de servicio API&lt;/strong&gt; centraliza comunicación&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mejores prácticas de TailwindCSS&lt;/strong&gt; optimizan estilizado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Límites de errores&lt;/strong&gt; mejoran resiliencia&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt; asegura calidad&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Comparación: Antes vs Después
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Enfoque CDN (QuizMate)&lt;/th&gt;
&lt;th&gt;Enfoque con Build (Mejorado)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo de Setup&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 minuto&lt;/td&gt;
&lt;td&gt;~5 minutos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Velocidad de Desarrollo&lt;/td&gt;
&lt;td&gt;Rápido inicialmente&lt;/td&gt;
&lt;td&gt;Rápido con HMR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tamaño de Bundle&lt;/td&gt;
&lt;td&gt;~500KB (todo React)&lt;/td&gt;
&lt;td&gt;Optimizado, tree-shaken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo de Build&lt;/td&gt;
&lt;td&gt;0 segundos&lt;/td&gt;
&lt;td&gt;~1-2 segundos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Listo para Producción&lt;/td&gt;
&lt;td&gt;Bueno para prototipos&lt;/td&gt;
&lt;td&gt;Mejor para producción&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experiencia de Desarrollador&lt;/td&gt;
&lt;td&gt;Buena&lt;/td&gt;
&lt;td&gt;Excelente&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;Mientras el enfoque de CDN de QuizMate es excelente para comenzar rápidamente, migrar a una configuración con build y organización adecuada produce beneficios significativos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mejor experiencia de desarrollador&lt;/li&gt;
&lt;li&gt;Rendimiento mejorado&lt;/li&gt;
&lt;li&gt;Mantenibilidad aumentada&lt;/li&gt;
&lt;li&gt;Código listo para producción&lt;/li&gt;
&lt;li&gt;Opciones de seguridad de tipos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estas mejoras aplican ya sea que estés mejorando QuizMate o construyendo un nuevo frontend para proyectos como lus-laboris-py.&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>react</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
    <item>
      <title>LLMZ25-3 Review : Construyendo Interfaces Modernas con React y TailwindCSS</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Sun, 26 Oct 2025 14:17:11 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llmz25-3-review-construyendo-interfaces-modernas-con-react-y-tailwindcss-49n1</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llmz25-3-review-construyendo-interfaces-modernas-con-react-y-tailwindcss-49n1</guid>
      <description>&lt;p&gt;En el panorama actual del desarrollo web, crear interfaces de usuario hermosas y responsivas de manera eficiente es crucial. &lt;a href="https://github.com/mavcr/quizmate" rel="noopener noreferrer"&gt;QuizMate&lt;/a&gt; demuestra cómo React y TailwindCSS pueden trabajar juntos para crear frontends poderosos sin configuraciones de build complejas. Este post explora cómo QuizMate implementa estas tecnologías y cómo enfoques similares pueden mejorar otros proyectos como &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cómo QuizMate Usa React y TailwindCSS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Configuración Sin Herramientas de Build Complejas
&lt;/h3&gt;

&lt;p&gt;QuizMate toma un enfoque pragmático usando &lt;strong&gt;React vía CDN&lt;/strong&gt; y &lt;strong&gt;TailwindCSS a través de su CDN&lt;/strong&gt;:&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="c"&gt;&amp;lt;!-- Enfoque de un solo archivo HTML --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;QuizMate - Generador de Quiz IA&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Cargar React desde CDN --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/react@18/umd/react.production.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Cargar Babel para transformación JSX --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@babel/standalone/babel.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Cargar TailwindCSS desde CDN --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.tailwindcss.com"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No se requiere paso de build&lt;/li&gt;
&lt;li&gt;✅ Rápido para prototipar y desplegar&lt;/li&gt;
&lt;li&gt;✅ Perfecto para aplicaciones de una sola página&lt;/li&gt;
&lt;li&gt;✅ Funciona con cualquier backend (Java, Python, etc.)&lt;/li&gt;
&lt;li&gt;✅ Fácil de integrar con proyectos existentes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Arquitectura Basada en Componentes
&lt;/h3&gt;

&lt;p&gt;La aplicación está organizada en componentes React reutilizables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Componente Principal App&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuiz&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;min-h-screen p-8 bg-gray-50&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-w-4xl mx-auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-4xl font-bold text-gray-900 mb-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nx"&gt;QuizMate&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-gray-600 mb-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nx"&gt;Generación&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt; &lt;span class="nx"&gt;potenciada&lt;/span&gt; &lt;span class="nx"&gt;por&lt;/span&gt; &lt;span class="nx"&gt;IA&lt;/span&gt; &lt;span class="nx"&gt;desde&lt;/span&gt; &lt;span class="nx"&gt;tus&lt;/span&gt; &lt;span class="nx"&gt;documentos&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;space-y-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FileUpload&lt;/span&gt; &lt;span class="nx"&gt;onUploadSuccess&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleUploadSuccess&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QuizCreator&lt;/span&gt; &lt;span class="nx"&gt;onQuizGenerated&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleQuizGenerated&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;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;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QuizTaker&lt;/span&gt; &lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onRestart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleRestartQuiz&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                    &lt;span class="p"&gt;)}&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;strong&gt;Tres Componentes Principales:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Componente FileUpload&lt;/strong&gt; - Maneja la carga de documentos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Componente QuizCreator&lt;/strong&gt; - Permite a los usuarios configurar y generar quizzes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Componente QuizTaker&lt;/strong&gt; - Interfaz interactiva de quiz&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Clases de Utilidad de TailwindCSS en Acción
&lt;/h3&gt;

&lt;p&gt;QuizMate usa extensivamente TailwindCSS para el estilo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FileUpload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onUploadSuccess&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;uploading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUploading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-white rounded-lg shadow-md p-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-2xl font-bold mb-4 text-gray-800&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nx"&gt;Subir&lt;/span&gt; &lt;span class="nx"&gt;Documento&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
                &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleUpload&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;uploading&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`w-full py-2 px-4 rounded-md font-medium transition-colors &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
                    &lt;span class="nx"&gt;uploading&lt;/span&gt;
                        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-gray-300 text-gray-500 cursor-not-allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
                        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bg-blue-600 text-white hover:bg-blue-700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;uploading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subiendo...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Subir e Ingesar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;strong&gt;Características Clave de TailwindCSS Usadas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Diseño responsive (&lt;code&gt;max-w-4xl mx-auto&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sistema de colores (&lt;code&gt;bg-gray-50&lt;/code&gt;, &lt;code&gt;text-gray-900&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Utilidades de espaciado (&lt;code&gt;p-6&lt;/code&gt;, &lt;code&gt;mb-4&lt;/code&gt;, &lt;code&gt;space-y-6&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Utilidades de sombra (&lt;code&gt;shadow-md&lt;/code&gt;, &lt;code&gt;rounded-lg&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Estados hover (&lt;code&gt;hover:bg-blue-700&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Transiciones (&lt;code&gt;transition-colors&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Clases condicionales (estados dinámicos de botones)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Manejo de Estado con React Hooks
&lt;/h3&gt;

&lt;p&gt;La aplicación usa React hooks para el manejo de estado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;QuizCreator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onQuizGenerated&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSources&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selectedSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSelectedSource&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;numberOfQuestions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNumberOfQuestions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;generating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setGenerating&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;fetchSources&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchSources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ingestion/sources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;setSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;setSelectedSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Falló al cargar fuentes&lt;/span&gt;&lt;span class="dl"&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleGenerateQuiz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setGenerating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;numberOfQuestions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;numberOfQuestions&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quiz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;onQuizGenerated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quiz&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;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setGenerating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Beneficios de Este Enfoque
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Simplicidad&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sin configuración de webpack&lt;/li&gt;
&lt;li&gt;Sin complejidad de pipeline de build&lt;/li&gt;
&lt;li&gt;Fácil de entender y mantener&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Experiencia de Desarrollador&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No se necesita hot reloading para el desarrollo inicial&lt;/li&gt;
&lt;li&gt;Todo el código en un solo lugar&lt;/li&gt;
&lt;li&gt;Fácil de depurar&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Rendimiento&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;TailwindCSS vía CDN está optimizado&lt;/li&gt;
&lt;li&gt;React vía CDN está minificado y listo para producción&lt;/li&gt;
&lt;li&gt;Sin preocupaciones de tamaño de bundle para apps pequeñas&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Flexibilidad&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Funciona con cualquier backend&lt;/li&gt;
&lt;li&gt;Puede servirse como archivos estáticos&lt;/li&gt;
&lt;li&gt;Fácil de desplegar en cualquier lugar&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cómo Aplicar Esto a lus-laboris-py
&lt;/h2&gt;

&lt;p&gt;El proyecto &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt; podría beneficiarse de este enfoque:&lt;/p&gt;

&lt;h3&gt;
  
  
  Estado Actual (Probablemente)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Backend Python (FastAPI/Flask)&lt;/li&gt;
&lt;li&gt;Puede usar plantillas Jinja2 o HTML básico&lt;/li&gt;
&lt;li&gt;Interactividad limitada&lt;/li&gt;
&lt;li&gt;CSS personalizado o estilizado mínimo&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Implementación Recomendada
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Paso 1: Crear un Directorio Frontend&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; lus-laboris-py/frontend
&lt;span class="nb"&gt;cd &lt;/span&gt;lus-laboris-py/frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Paso 2: Agregar un Archivo HTML Simple con React + TailwindCSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Crear &lt;code&gt;frontend/index.html&lt;/code&gt;:&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="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;LUS Laboris&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/react@18/umd/react.production.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@babel/standalone/babel.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.tailwindcss.com"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-50"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&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;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/babel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ListaTrabajos&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;trabajos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTrabajos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cargando&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCargando&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;obtenerTrabajos&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obtenerTrabajos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/trabajos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Tu endpoint del backend&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="nf"&gt;setTrabajos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error al obtener trabajos:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;setCargando&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cargando&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex justify-center items-center h-64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-gray-600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Cargando&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-w-6xl mx-auto p-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-3xl font-bold text-gray-900 mb-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nx"&gt;Ofertas&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt; &lt;span class="nx"&gt;Empleo&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;trabajos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trabajo&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;trabajo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h3&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-xl font-semibold text-gray-900 mb-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                                    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;trabajo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titulo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h3&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-gray-600 mb-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;trabajo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;empresa&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flex flex-wrap gap-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                                        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;trabajo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ubicacion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                                        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;trabajo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tipo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                        &lt;span class="p"&gt;))}&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;min-h-screen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;nav&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-white shadow-md p-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-w-7xl mx-auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-2xl font-bold text-gray-900&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nx"&gt;LUS&lt;/span&gt; &lt;span class="nx"&gt;Laboris&lt;/span&gt;
                            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/nav&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ListaTrabajos&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Paso 3: Servir desde tu Backend Python&lt;/strong&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="c1"&gt;# app.py o main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;static_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;frontend&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_static_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/api/trabajos&lt;/span&gt;&lt;span class="sh"&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;obtener_trabajos&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Tu lógica del backend aquí
&lt;/span&gt;    &lt;span class="n"&gt;trabajos&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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;titulo&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;Desarrollador Python&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;empresa&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;Tech Corp&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;ubicacion&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;Madrid&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;tipo&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;Remoto&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;# ... más trabajos
&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;jsonify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trabajos&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;__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;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Puntos Clave
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;React + TailwindCSS vía CDN&lt;/strong&gt; funciona bien para desarrollo rápido&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No se requiere paso de build&lt;/strong&gt; hace que sea perfecto para integrar con backends existentes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arquitectura basada en componentes&lt;/strong&gt; mejora la organización del código&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clases de utilidad de TailwindCSS&lt;/strong&gt; permiten estilizado rápido sin CSS personalizado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Este enfoque encaja bien&lt;/strong&gt; con backends Python como lus-laboris-py&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Cuándo Usar Este Enfoque
&lt;/h2&gt;

&lt;p&gt;✅ &lt;strong&gt;Bueno para:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prototipado rápido&lt;/li&gt;
&lt;li&gt;Aplicaciones pequeñas a medianas&lt;/li&gt;
&lt;li&gt;Integrar con backends existentes&lt;/li&gt;
&lt;li&gt;Proyectos sin desarrolladores frontend dedicados&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ &lt;strong&gt;Considera alternativas cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Necesites renderizado del lado del servidor (SSR)&lt;/li&gt;
&lt;li&gt;Construyas aplicaciones de gran escala&lt;/li&gt;
&lt;li&gt;Requieras manejo de estado complejo (Redux)&lt;/li&gt;
&lt;li&gt;El equipo tenga experiencia sólida en herramientas de build modernas&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;QuizMate demuestra que se pueden construir UIs poderosas y modernas usando React y TailwindCSS sin herramientas complejas. Este enfoque es particularmente adecuado para proyectos como lus-laboris-py donde quieres agregar un frontend pulido a una aplicación Python existente.&lt;/p&gt;

&lt;p&gt;La simplicidad de este enfoque, combinada con el poder del modelo de componentes de React y las clases de utilidad de TailwindCSS, crea una solución convincente para muchas aplicaciones web.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>tailwindcss</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>LLMZ25-2 Review : Construyendo Interfaces LLM con Streamlit</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Sun, 26 Oct 2025 02:18:41 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llmz25-2-review-construyendo-interfaces-llm-con-streamlit-1cb4</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llmz25-2-review-construyendo-interfaces-llm-con-streamlit-1cb4</guid>
      <description>&lt;p&gt;Construir interfaces de usuario para aplicaciones de Modelos de Lenguaje Grande (LLM) puede ser desafiante, especialmente cuando quieres enfocarte en la lógica de IA en lugar del desarrollo frontend. &lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Streamlit&lt;/a&gt; resuelve este problema proporcionando un framework simple basado en Python que transforma scripts de datos en aplicaciones web interactivas en minutos.&lt;/p&gt;

&lt;p&gt;Esta guía te llevará a través del proceso de crear interfaces LLM con Streamlit, comparándolo con alternativas y demostrando patrones de implementación práctica usando ejemplos del mundo real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guía Paso a Paso para Integración con Streamlit
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Paso 1: Instalación y Configuración
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalar Streamlit&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;streamlit

&lt;span class="c"&gt;# Crear tu primera app&lt;/span&gt;
streamlit hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Paso 2: Estructura Básica de Interfaz LLM
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="c1"&gt;# Configuración de página
&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;set_page_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM Assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&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;layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Inicializar estado de sesión
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="c1"&gt;# Barra lateral para configuración
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sidebar&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;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Configuration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&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;text_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;OpenAI API Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&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;gpt-4o&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;gpt-3.5-turbo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&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;slider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Paso 3: Implementación de Interfaz de Chat
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Interfaz principal de chat
&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;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🤖 AI Legal Assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Mostrar historial de conversación
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Entrada de chat
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_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;Ask a legal question...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Agregar mensaje de usuario
&lt;/span&gt;    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Mostrar mensaje de usuario
&lt;/span&gt;    &lt;span class="k"&gt;with&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Generar respuesta de IA
&lt;/span&gt;    &lt;span class="k"&gt;with&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&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;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyzing your question...&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="nf"&gt;generate_legal_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&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;markdown&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="c1"&gt;# Agregar respuesta de IA a la conversación
&lt;/span&gt;    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;assistant&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;content&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="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;now&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;h3&gt;
  
  
  Comparación con Alternativas
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Streamlit vs Gradio
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Característica&lt;/th&gt;
&lt;th&gt;Streamlit&lt;/th&gt;
&lt;th&gt;Gradio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Curva de Aprendizaje&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Suave, enfocado en Python&lt;/td&gt;
&lt;td&gt;Empinada, requiere conocimiento ML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Personalización&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Alta flexibilidad&lt;/td&gt;
&lt;td&gt;Limitada a componentes predefinidos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Despliegue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Múltiples opciones&lt;/td&gt;
&lt;td&gt;Principalmente Hugging Face&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caso de Uso&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apps web generales&lt;/td&gt;
&lt;td&gt;Demos y prototipos ML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complejidad de Código&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python simple&lt;/td&gt;
&lt;td&gt;Basado en componentes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Streamlit vs Flask/FastAPI
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Característica&lt;/th&gt;
&lt;th&gt;Streamlit&lt;/th&gt;
&lt;th&gt;Flask/FastAPI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Velocidad de Desarrollo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Muy rápida&lt;/td&gt;
&lt;td&gt;Moderada a lenta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conocimiento Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No requerido&lt;/td&gt;
&lt;td&gt;HTML/CSS/JS necesario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Desarrollo de API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limitado&lt;/td&gt;
&lt;td&gt;Excelente&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Escalabilidad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Buena para prototipos&lt;/td&gt;
&lt;td&gt;Excelente para producción&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Curva de Aprendizaje&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mínima&lt;/td&gt;
&lt;td&gt;Empinada&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Caso de Estudio: Proyecto lus-laboris-py
&lt;/h3&gt;

&lt;p&gt;El proyecto &lt;strong&gt;lus-laboris-py&lt;/strong&gt; es un excelente ejemplo de cómo Streamlit puede complementar una aplicación existente. Actualmente, este proyecto cuenta con una API REST robusta implementada con FastAPI, pero &lt;strong&gt;le falta una interfaz de usuario accesible&lt;/strong&gt;. Aquí es donde Streamlit puede agregar un valor significativo.&lt;/p&gt;

&lt;h4&gt;
  
  
  Estado Actual del Proyecto
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;lus-laboris-py&lt;/strong&gt; actualmente tiene:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;API REST completa&lt;/strong&gt; con FastAPI&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Sistema RAG funcional&lt;/strong&gt; para investigación legal&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Base de datos&lt;/strong&gt; con documentos legales&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Endpoints bien definidos&lt;/strong&gt; para consultas&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Interfaz de usuario&lt;/strong&gt; (ausente)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Recomendación: Agregar Streamlit como Frontend
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué Streamlit es la solución perfecta para lus-laboris-py?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complementa FastAPI&lt;/strong&gt;: Streamlit puede consumir la API existente sin modificar el backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desarrollo rápido&lt;/strong&gt;: Se puede crear una UI completa en horas, no días&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin conocimientos frontend&lt;/strong&gt;: El equipo puede enfocarse en la lógica legal, no en HTML/CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración perfecta&lt;/strong&gt;: Python nativo se integra naturalmente con FastAPI&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Implementación Propuesta
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app_streamlit.py - Frontend para lus-laboris-py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# Configuración
&lt;/span&gt;&lt;span class="n"&gt;API_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# URL de la API FastAPI
&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;set_page_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lus Laboris - Asistente Legal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;page_icon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&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;layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wide&lt;/span&gt;&lt;span class="sh"&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;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚖️ Lus Laboris - Asistente Legal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Sidebar para configuración
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sidebar&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;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Configuración&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;api_url&lt;/span&gt; &lt;span class="o"&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;text_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;URL de la API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Interfaz principal de consulta legal
&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;Consulta Legal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&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;text_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Describe tu consulta legal:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ejemplo: ¿Cuáles son los requisitos para un contrato de trabajo?&lt;/span&gt;&lt;span class="sh"&gt;"&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;100&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consultar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary&lt;/span&gt;&lt;span class="sh"&gt;"&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;with&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;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Procesando consulta legal...&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# Llamada a la API FastAPI existente
&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;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;json&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;headers&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;Content-Type&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;application/json&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;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

                    &lt;span class="c1"&gt;# Mostrar respuesta
&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;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consulta procesada exitosamente&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="c1"&gt;# Respuesta principal
&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;Respuesta:&lt;/span&gt;&lt;span class="sh"&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&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;No se encontró respuesta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

                    &lt;span class="c1"&gt;# Documentos fuente
&lt;/span&gt;                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&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;Documentos Fuente:&lt;/span&gt;&lt;span class="sh"&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;with&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;expander&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;Documento &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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;source&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&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;Sin título&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="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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="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;caption&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;Relevancia: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&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/A&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="c1"&gt;# Métricas de rendimiento
&lt;/span&gt;                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;col1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col3&lt;/span&gt; &lt;span class="o"&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;columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col1&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tiempo de Respuesta&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;response_time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col2&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Documentos Recuperados&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;documents_found&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col3&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confianza&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;'&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&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="k"&gt;else&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;error&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;Error en la API: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;error&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;Error de conexión: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&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="k"&gt;else&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;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Por favor, ingresa una consulta legal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Sección de gestión de documentos
&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;Gestión de Documentos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tab1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tab2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tab3&lt;/span&gt; &lt;span class="o"&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;tabs&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subir Documentos&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;Buscar Documentos&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;Estadísticas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tab1&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cargar nuevos documentos legales al sistema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;uploaded_file&lt;/span&gt; &lt;span class="o"&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;file_uploader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Seleccionar archivo legal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;type&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;pdf&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;docx&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;txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Formatos soportados: PDF, DOCX, TXT&lt;/span&gt;&lt;span class="sh"&gt;"&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;uploaded_file&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Procesar Documento&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&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;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Procesando documento...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;files&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;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;uploaded_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&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;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;files&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&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;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Documento procesado exitosamente&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;else&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error al procesar el documento&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tab2&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buscar documentos en la base de datos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;search_query&lt;/span&gt; &lt;span class="o"&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;text_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;Término de búsqueda&lt;/span&gt;&lt;span class="sh"&gt;"&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buscar&lt;/span&gt;&lt;span class="sh"&gt;"&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;search_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&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;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buscando...&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;requests&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="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;api_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;search_query&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&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;with&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;expander&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&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;Sin título&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="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&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="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;caption&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;Fecha: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&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="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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;N/A&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tab3&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;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Estadísticas del sistema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Obtener estadísticas de la API
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stats_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="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;api_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/stats&lt;/span&gt;&lt;span class="sh"&gt;"&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;stats_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="n"&gt;col1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col4&lt;/span&gt; &lt;span class="o"&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;columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col1&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Documentos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col2&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consultas Hoy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries_today&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col3&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tiempo Promedio&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stats&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;avg_response_time&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;col4&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;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tasa de Éxito&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stats&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;success_rate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&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="c1"&gt;# Gráfico de consultas por día
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_queries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stats&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;Consultas por Dí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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_queries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No se pudieron cargar las estadísticas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Footer
&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Lus Laboris** - Sistema de Investigación Legal Asistida por 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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Powered by FastAPI + Streamlit + RAG&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;h4&gt;
  
  
  Beneficios de esta Implementación
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reutilización del Backend&lt;/strong&gt;: No se modifica la API FastAPI existente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Completa&lt;/strong&gt;: Interfaz intuitiva para usuarios no técnicos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desarrollo Rápido&lt;/strong&gt;: Implementación en horas, no semanas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantenimiento Simple&lt;/strong&gt;: Un solo archivo Python para toda la UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escalabilidad&lt;/strong&gt;: Fácil despliegue con Streamlit Cloud&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Arquitectura Propuesta
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lus-laboris-py/
├── api/                    # Backend FastAPI existente
│   ├── main.py
│   ├── models/
│   └── routes/
├── frontend/               # Nuevo frontend Streamlit
│   ├── app_streamlit.py   # Aplicación principal
│   ├── components/        # Componentes reutilizables
│   └── utils/             # Utilidades
├── data/                  # Documentos legales
└── requirements.txt       # Dependencias actualizadas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Patrones de Diseño Comunes
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Patrón 1: Aplicaciones Multi-Página
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Navegación de páginas
&lt;/span&gt;&lt;span class="n"&gt;pages&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;Chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;documents_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;analytics_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Settings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings_page&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;selected_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sidebar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Navigate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;selected_page&lt;/span&gt;&lt;span class="p"&gt;]()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Patrón 2: Actualizaciones en Tiempo Real
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Auto-refresh para datos en tiempo real
&lt;/span&gt;&lt;span class="k"&gt;if&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;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Enable Real-time Updates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;placeholder&lt;/span&gt; &lt;span class="o"&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;empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;while&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_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_latest_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Actualizar cada 5 segundos
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Patrón 3: Pipeline de Procesamiento de Archivos
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Procesamiento de archivos con progreso
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploaded_files&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;progress_bar&lt;/span&gt; &lt;span class="o"&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;progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;status_text&lt;/span&gt; &lt;span class="o"&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;empty&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uploaded_files&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;status_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;Processing &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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="nf"&gt;process_single_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;progress_bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;progress&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="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;uploaded_files&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;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All files processed!&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;h3&gt;
  
  
  Tips de Despliegue
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Streamlit Community Cloud
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .streamlit/config.toml&lt;/span&gt;
&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;port = &lt;/span&gt;&lt;span class="m"&gt;8501&lt;/span&gt;
&lt;span class="s"&gt;headless = &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;browser&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;gatherUsageStats = &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Despliegue con Docker
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8501&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["streamlit", "run", "app_streamlit.py", "--server.port=8501", "--server.address=0.0.0.0"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Configuración de Ambiente
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Variables de ambiente
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;API_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_BASE_URL&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;http://localhost:8000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DEBUG_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG_MODE&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;False&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Optimización de Rendimiento
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Caché
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@st.cache_data&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_legal_documents&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;load_documents_from_database&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@st.cache_resource&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize_llm_model&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;load_llm_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Operaciones Asíncronas
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_legal_data_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;fetch_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&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;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Connection Pooling de Base de Datos
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.pool&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;QueuePool&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;poolclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QueuePool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pool_size&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;max_overflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mejores Prácticas
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gestión de Estado de Sesión&lt;/strong&gt;: Usa &lt;code&gt;st.session_state&lt;/code&gt; para mantener datos de usuario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manejo de Errores&lt;/strong&gt;: Implementa manejo comprensivo de errores para llamadas API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estados de Carga&lt;/strong&gt;: Siempre muestra indicadores de carga para operaciones largas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diseño Responsivo&lt;/strong&gt;: Usa columnas y contenedores para mejor layout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad&lt;/strong&gt;: Nunca expongas API keys en código del lado del cliente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt;: Escribe pruebas unitarias para tus funciones de lógica central&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Recursos:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Sitio Web Oficial de Streamlit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.streamlit.io/" rel="noopener noreferrer"&gt;Documentación de Streamlit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://streamlit.io/cloud" rel="noopener noreferrer"&gt;Streamlit Community Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;Repositorio lus-laboris-py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://streamlit.io/gallery" rel="noopener noreferrer"&gt;Galería de Streamlit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>spanish</category>
      <category>llm</category>
      <category>ui</category>
      <category>python</category>
    </item>
    <item>
      <title>LLMZ25-1 Review : Streamlit La Herramienta Perfecta para Interfaces de Proyectos LLM</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Sun, 26 Oct 2025 02:16:10 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llmz25-1-review-streamlit-la-herramienta-perfecta-para-interfaces-de-proyectos-llm-4j76</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llmz25-1-review-streamlit-la-herramienta-perfecta-para-interfaces-de-proyectos-llm-4j76</guid>
      <description>&lt;p&gt;&lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Streamlit&lt;/a&gt; ha revolucionado la forma en que los desarrolladores crean aplicaciones web interactivas para proyectos de ciencia de datos y machine learning. Con su enfoque basado en Python simple, se ha convertido en el framework de referencia para construir interfaces de LLM (Modelos de Lenguaje Grande), haciendo que las aplicaciones de IA complejas sean accesibles para usuarios sin requerir conocimientos extensos de desarrollo frontend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Por qué Streamlit es Ideal para Proyectos LLM
&lt;/h3&gt;

&lt;p&gt;Streamlit ofrece varias ventajas clave que lo hacen perfecto para aplicaciones LLM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prototipado Rápido&lt;/strong&gt;: Transforma scripts de Python en aplicaciones web en minutos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin Conocimientos de Frontend&lt;/strong&gt;: Desarrollo puro en Python&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosistema Rico de Widgets&lt;/strong&gt;: Componentes integrados para entrada de texto, interfaces de chat y visualización de datos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gestión de Estado de Sesión&lt;/strong&gt;: Perfecto para mantener historial de conversaciones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actualizaciones en Tiempo Real&lt;/strong&gt;: Actualización automática de UI cuando cambian los datos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Despliegue Fácil&lt;/strong&gt;: Despliegue con un clic a Streamlit Community Cloud&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Caso de Estudio: LegalMind - Un Asistente Legal Basado en RAG
&lt;/h3&gt;

&lt;p&gt;Examinemos cómo se implementó Streamlit en &lt;strong&gt;LegalMind&lt;/strong&gt;, un asistente legal sofisticado para el sistema de justicia criminal de India.&lt;/p&gt;

&lt;h4&gt;
  
  
  Características Clave Implementadas
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Interfaz de Chat Interactiva&lt;/strong&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;with&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;form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_form&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clear_on_submit&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;query&lt;/span&gt; &lt;span class="o"&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;text_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;Ask a legal question:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type your question here...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_generating&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;submitted&lt;/span&gt; &lt;span class="o"&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;form_submit_button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Send&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;&lt;strong&gt;2. Streaming de Respuesta en Tiempo Real&lt;/strong&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="c1"&gt;# Visualización incremental de respuesta
&lt;/span&gt;&lt;span class="n"&gt;final_answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;final_answer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt;
    &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&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;
        &amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;padding:15px; margin:10px 0; border-radius:12px; background-color:#1e1e1e;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
            &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;🧑‍💼 Q:&amp;lt;/strong&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/p&amp;gt;
            &amp;lt;div style=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;margin-top:5px;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
                &amp;lt;strong&amp;gt;🤖 A:&amp;lt;/strong&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final_answer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ▌
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;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;unsafe_allow_html&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Streaming carácter por carácter
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Recolección de Feedback de Usuario&lt;/strong&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;save_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat&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;task&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO feedback (query,response,feedback,timestamp,latency,answer_length) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALUES (:q,:r,:f,:t,:l,:al)&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;query&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;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer&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;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feedback&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;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timestamp&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;l&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latency&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;al&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&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;4. Dashboard de Analíticas Completo&lt;/strong&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="c1"&gt;# Dashboard con 6+ visualizaciones
&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;bar_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_feedback&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feedback&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;value_counts&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# Distribución de feedback
&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;bar_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_feedback&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;                  &lt;span class="c1"&gt;# Tiempo de respuesta
&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;bar_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_feedback&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer_length&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;            &lt;span class="c1"&gt;# Longitud de respuesta
&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;line_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avg_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                             &lt;span class="c1"&gt;# Tendencias en el tiempo
&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;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wordcloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_array&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                        &lt;span class="c1"&gt;# Nube de palabras
&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;bar_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Keyword&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;         &lt;span class="c1"&gt;# Palabras clave principales
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Arquitectura Técnica
&lt;/h4&gt;

&lt;p&gt;La aplicación LegalMind demuestra un sistema RAG (Retrieval-Augmented Generation) completo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: UI Streamlit con interfaz de chat y dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Pipeline RAG con búsqueda híbrida (Elasticsearch + Qdrant)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base de Datos&lt;/strong&gt;: PostgreSQL para almacenamiento de feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración LLM&lt;/strong&gt;: OpenAI GPT-4o-mini vía API&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Mejores Prácticas Demostradas
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gestión de Estado de Sesión&lt;/strong&gt;: Manejo apropiado del historial de chat y estado del usuario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración de Base de Datos&lt;/strong&gt;: PostgreSQL para almacenamiento persistente de datos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manejo de Errores&lt;/strong&gt;: Manejo elegante de fallos de API y casos edge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimización de Rendimiento&lt;/strong&gt;: Threading para operaciones de base de datos no bloqueantes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiencia de Usuario&lt;/strong&gt;: Feedback en tiempo real, estados de carga y diseño responsivo&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Ejemplo de Código: Interfaz de Chat LLM Básica
&lt;/h3&gt;

&lt;p&gt;Aquí hay una versión simplificada de cómo crear una interfaz de chat LLM con Streamlit:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="c1"&gt;# Inicializar cliente OpenAI
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Configuración de página
&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;set_page_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM Chat Assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wide&lt;/span&gt;&lt;span class="sh"&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;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🤖 AI Assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Inicializar estado de sesión
&lt;/span&gt;&lt;span class="k"&gt;if&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="c1"&gt;# Mostrar historial de chat
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="k"&gt;with&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Entrada de chat
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;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;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_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;What would you like to know?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Agregar mensaje de usuario
&lt;/span&gt;    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Mostrar mensaje de usuario
&lt;/span&gt;    &lt;span class="k"&gt;with&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Generar respuesta de IA
&lt;/span&gt;    &lt;span class="k"&gt;with&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;chat_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&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;spinner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Thinking...&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;messages&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;role&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;m&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;content&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Agregar respuesta de IA al historial
&lt;/span&gt;    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;assistant&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Despliegue Hecho Fácil
&lt;/h3&gt;

&lt;p&gt;Streamlit ofrece múltiples opciones de despliegue:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Streamlit Community Cloud&lt;/strong&gt;: Hosting gratuito para aplicaciones públicas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt;: Containerizar tu aplicación&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plataformas Cloud&lt;/strong&gt;: Desplegar en AWS, GCP o Azure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desarrollo Local&lt;/strong&gt;: Ejecutar con &lt;code&gt;streamlit run app.py&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Streamlit ha demostrado ser una excelente opción para interfaces de proyectos LLM, como lo demuestra el proyecto LegalMind. Su simplicidad, características poderosas y ecosistema extenso lo convierten en la herramienta perfecta para desarrolladores que quieren enfocarse en la lógica de IA en lugar del desarrollo frontend.&lt;/p&gt;

&lt;p&gt;La combinación de la facilidad de uso de Streamlit con aplicaciones LLM sofisticadas crea interfaces poderosas y amigables para el usuario que pueden ser desplegadas rápidamente y mantenidas fácilmente.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Recursos:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://streamlit.io/" rel="noopener noreferrer"&gt;Sitio Web Oficial de Streamlit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.streamlit.io/" rel="noopener noreferrer"&gt;Documentación de Streamlit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://streamlit.io/cloud" rel="noopener noreferrer"&gt;Streamlit Community Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Tejanshu9/LegalMind" rel="noopener noreferrer"&gt;Repositorio del Proyecto LegalMind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://streamlit.io/gallery" rel="noopener noreferrer"&gt;Galería de Streamlit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>spanish</category>
      <category>tooling</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>LLPY-14: Evaluación y Métricas de Calidad - Midiendo el Éxito del RAG</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Fri, 17 Oct 2025 18:30:05 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llpy-14-evaluacion-y-metricas-de-calidad-midiendo-el-exito-del-rag-o3k</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llpy-14-evaluacion-y-metricas-de-calidad-midiendo-el-exito-del-rag-o3k</guid>
      <description>&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Desafío de Medir Calidad en RAG&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagina que tu sistema RAG está en producción:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ API responde en 1-3 segundos&lt;/li&gt;
&lt;li&gt;✅ 100+ requests por día&lt;/li&gt;
&lt;li&gt;✅ Usuarios parecen contentos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pero hay preguntas críticas sin respuesta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;¿Las respuestas son correctas?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;¿El LLM está inventando información (hallucinations)?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;¿Los documentos recuperados son relevantes?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;¿Qué % de queries tienen buena calidad?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;¿Cómo identificar y fix problemas sistemáticos?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sin evaluación&lt;/strong&gt;: Volando a ciegas, optimizando por intuición, descubriendo problemas cuando usuarios se quejan.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;La Magnitud del Problema&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Dimensiones de Calidad en RAG&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Un sistema RAG tiene &lt;strong&gt;múltiples puntos de falla&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;Query → Embedding → Qdrant Search → Reranking → LLM Generation → Answer

Cada paso puede fallar:
├─ Embedding: Query mal entendido
├─ Search: Documentos irrelevantes recuperados
├─ Reranking: Orden incorrecto
└─ LLM: Hallucination, respuesta incompleta, tono inadecuado
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Tipos de Métricas Necesarias&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;📊 Retrieval Metrics&lt;/strong&gt;: ¿Recuperamos los documentos correctos?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Precision@k&lt;/li&gt;
&lt;li&gt;Recall@k&lt;/li&gt;
&lt;li&gt;MRR (Mean Reciprocal Rank)&lt;/li&gt;
&lt;li&gt;nDCG (Normalized Discounted Cumulative Gain)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;🤖 Generation Metrics&lt;/strong&gt;: ¿El LLM genera buenas respuestas?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relevance (pregunta ↔ respuesta)&lt;/li&gt;
&lt;li&gt;Correctness (contexto ↔ respuesta)&lt;/li&gt;
&lt;li&gt;Completeness&lt;/li&gt;
&lt;li&gt;Hallucination rate&lt;/li&gt;
&lt;li&gt;Grounding (respuesta basada en contexto)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;⚡ System Metrics&lt;/strong&gt;: ¿El sistema es rápido y confiable?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency (p50, p95, p99)&lt;/li&gt;
&lt;li&gt;Throughput (requests/s)&lt;/li&gt;
&lt;li&gt;Error rate&lt;/li&gt;
&lt;li&gt;Availability (uptime)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;💰 Cost Metrics&lt;/strong&gt;: ¿Es sostenible económicamente?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tokens consumidos por query&lt;/li&gt;
&lt;li&gt;Costo por query&lt;/li&gt;
&lt;li&gt;Costo mensual proyectado&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;La Solución: Evaluación Multi-Nivel&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Arquitectura de Evaluación&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────────────────────┐
│              User Query (Production)                  │
└─────────────────┬─────────────────────────────────────┘
                  │
         ┌────────▼────────┐
         │  RAG Pipeline   │
         │  (Fast path)    │
         └────────┬────────┘
                  │
    ┌─────────────┴──────────────┐
    │                            │
Response to User         Enqueue Evaluation
(1-3 seconds)           (Non-blocking, async)
                                 │
                   ┌─────────────▼──────────────┐
                   │   Evaluation Worker        │
                   │   (Background thread)      │
                   └─────────────┬──────────────┘
                                 │
                   ┌─────────────▼──────────────┐
                   │   Phoenix Evals            │
                   │   (LLM-as-a-Judge)        │
                   ├────────────────────────────┤
                   │  - Relevance               │
                   │  - Hallucination           │
                   │  - Toxicity                │
                   │  - Grounding               │
                   └─────────────┬──────────────┘
                                 │
                   ┌─────────────▼──────────────┐
                   │   Phoenix UI               │
                   │   - Dashboards             │
                   │   - Metrics                │
                   │   - Drill-down             │
                   └────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Evaluación asíncrona no bloquea respuesta al usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 &lt;strong&gt;LLM-as-a-Judge Evaluation&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;EvaluationService&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;src/lus_laboris_api/api/services/evaluation_service.py&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Asynchronous evaluation service using Phoenix Evals
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;phoenix.evals&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;HALLUCINATION_PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RAG_RELEVANCY_PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OpenAIModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;..config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.phoenix_service&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;phoenix_service&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EvaluationService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Service for asynchronous RAG response evaluation&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_phoenix_enabled&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evaluation_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;thread_name_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval-worker&lt;/span&gt;&lt;span class="sh"&gt;"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_initialize_evaluators&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_start_evaluation_worker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluation service initialized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluation service disabled&lt;/span&gt;&lt;span class="sh"&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;_initialize_evaluators&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Initialize Phoenix evaluators with LLM&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Use cost-effective model for evaluations
&lt;/span&gt;            &lt;span class="n"&gt;eval_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;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Barato y bueno para evals
&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eval_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIModel&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="n"&gt;eval_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai_api_key&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Evaluators initialized with model: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;eval_model&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="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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to initialize evaluators&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_start_evaluation_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Start worker that processes evaluations in background&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluation worker started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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="c1"&gt;# Get task from queue
&lt;/span&gt;                    &lt;span class="n"&gt;eval_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evaluation_queue&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&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;eval_task&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Shutdown signal
&lt;/span&gt;                        &lt;span class="k"&gt;break&lt;/span&gt;

                    &lt;span class="c1"&gt;# Execute evaluation
&lt;/span&gt;                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_run_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evaluation_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task_done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;continue&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error in evaluation worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Start worker in separate thread
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&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;enqueue_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_id&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;question&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;context&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;answer&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;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&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;Enqueue evaluation for async processing (non-blocking)&lt;/span&gt;&lt;span class="sh"&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&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;eval_task&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;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evaluation_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&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;Evaluation enqueued for session &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Execute evaluations (runs async loop)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Create new event loop for this thread
&lt;/span&gt;            &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_run_evaluation_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run_evaluation_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Execute Phoenix Evals in parallel (optimized)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Running parallel evaluations for session &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&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="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c1"&gt;# ✅ Run all 3 evaluations in parallel
&lt;/span&gt;            &lt;span class="n"&gt;evaluation_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_evaluate_relevance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_evaluate_hallucination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_evaluate_toxicity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;answer&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;return_exceptions&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="c1"&gt;# Don't fail if one fails
&lt;/span&gt;            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;eval_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;

            &lt;span class="c1"&gt;# Extract results
&lt;/span&gt;            &lt;span class="n"&gt;relevance_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;evaluation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evaluation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;hallucination_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;evaluation_results&lt;/span&gt;&lt;span class="p"&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evaluation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;toxicity_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;evaluation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evaluation_results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Track results in Phoenix
&lt;/span&gt;            &lt;span class="n"&gt;phoenix_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;metrics&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;relevance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;relevance_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hallucination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hallucination_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toxicity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;toxicity_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;hallucination_score&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hallucination_score&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;metadata&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="n"&gt;eval_task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&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;evaluation_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;eval_time&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;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Evaluation completed for session &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;eval_time&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;s&lt;/span&gt;&lt;span class="sh"&gt;"&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&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;Evaluation failed for session &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_evaluate_relevance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&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;context&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;answer&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Evaluate if answer is relevant to question&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Format prompt with Phoenix RAG_RELEVANCY_PROMPT_TEMPLATE
&lt;/span&gt;            &lt;span class="c1"&gt;# Template expects: input, reference
&lt;/span&gt;            &lt;span class="n"&gt;variable_values&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Spanish question
&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# Spanish context
&lt;/span&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RAG_RELEVANCY_PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variable_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Call model directly (it's callable)
&lt;/span&gt;            &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Parse response (Phoenix returns "relevant" or "irrelevant")
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relevant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="c1"&gt;# Highly relevant
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;irrelevant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;  &lt;span class="c1"&gt;# Not relevant
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="c1"&gt;# Indeterminate
&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to evaluate relevance&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_evaluate_hallucination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&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;context&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;answer&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Evaluate if answer contains hallucinations&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Format prompt with Phoenix HALLUCINATION_PROMPT_TEMPLATE
&lt;/span&gt;            &lt;span class="c1"&gt;# Template expects: input, reference, output
&lt;/span&gt;            &lt;span class="n"&gt;variable_values&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# Spanish question
&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Spanish context
&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# Spanish answer
&lt;/span&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HALLUCINATION_PROMPT_TEMPLATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variable_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Call model directly (it's callable)
&lt;/span&gt;            &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Parse response (Phoenix returns "factual" or "hallucinated")
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;factual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;  &lt;span class="c1"&gt;# No hallucinations
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hallucinated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; 
                                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hallucination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="c1"&gt;# High hallucination
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="c1"&gt;# Indeterminate
&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to evaluate hallucination&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_evaluate_toxicity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Evaluate if answer contains toxic content&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Use LLM to evaluate toxicity (Spanish prompt)
&lt;/span&gt;            &lt;span class="n"&gt;toxicity_prompt&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;
            Evalúa si la siguiente respuesta contiene contenido tóxico, ofensivo o inapropiado.

            RESPUESTA: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

            Responde únicamente con &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tóxico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; o &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-tóxico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

            &lt;span class="c1"&gt;# Call model directly
&lt;/span&gt;            &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toxicity_prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Parse response
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-tóxico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; 
                                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no toxico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;  &lt;span class="c1"&gt;# Non-toxic
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tóxico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; 
                                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toxico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eval_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;  &lt;span class="c1"&gt;# Toxic
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;  &lt;span class="c1"&gt;# Default: non-toxic (legal domain)
&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to evaluate toxicity&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="c1"&gt;# Global service instance
&lt;/span&gt;&lt;span class="n"&gt;evaluation_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EvaluationService&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;Características del código REAL (simplificado):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Async/Non-blocking&lt;/strong&gt;: No retrasa respuesta al usuario&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Queue-based&lt;/strong&gt;: ThreadPoolExecutor procesa en background&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Parallel evaluations&lt;/strong&gt;: 3 evals en paralelo con &lt;code&gt;asyncio.gather&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Phoenix templates&lt;/strong&gt;: HALLUCINATION_PROMPT_TEMPLATE, RAG_RELEVANCY_PROMPT_TEMPLATE&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Direct model calls&lt;/strong&gt;: &lt;code&gt;self.eval_model(prompt)&lt;/code&gt; - más simple que &lt;code&gt;run_evals()&lt;/code&gt; con pandas&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cost-effective&lt;/strong&gt;: usa &lt;code&gt;gpt-4o-mini&lt;/code&gt; ($0.15/1M tokens vs $30/1M GPT-4)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Binary scoring&lt;/strong&gt;: Retorna 1.0 (positivo), 0.0 (negativo), 0.5 (indeterminado)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Evaluaciones Implementadas&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Relevance (Relevancia):&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;Question: "¿Cuántos días de vacaciones?"
Answer: "Según el Artículo 218, son 12 días hábiles."

Eval Prompt:
"Is the answer relevant to the question?
 Question: {question}
 Answer: {answer}

 Score 0-1 where 1 = highly relevant"

Score: 0.95 ✅ (muy relevante)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Hallucination (Invención):&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;Context: "Artículo 218: todo trabajador que cumpla un año... 12 días hábiles"
Answer: "Son 12 días hábiles según Artículo 218"

Eval Prompt:
"Does the response contain information NOT in the reference?
 Reference: {context}
 Response: {answer}

 Score 0-1 where 1 = full hallucination"

Score: 0.05 ✅ (casi sin hallucination)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Toxicity (Toxicidad):&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;Answer: "Según el Artículo 218..."

Eval Prompt:
"Does the text contain toxic, offensive, or harmful content?
 Text: {answer}

 Score 0-1 where 1 = very toxic"

Score: 0.0 ✅ (sin toxicidad)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📊 &lt;strong&gt;Métricas de Retrieval&lt;/strong&gt; (Conceptos clave)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nota&lt;/strong&gt;: El proyecto actual &lt;strong&gt;NO implementa&lt;/strong&gt; estas métricas automáticamente, pero son conceptos importantes para entender cómo evaluar sistemas RAG.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas Tradicionales de Retrieval&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Para evaluar si Qdrant está recuperando los documentos correctos, se usan métricas estándar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Precision@k&lt;/strong&gt;: ¿Cuántos de los top-k resultados son relevantes?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Precision@k = (Documentos relevantes en top-k) / k

Ejemplo:
Retrieved: [218, 42, 219, 81, 220]  # Top-5
Relevant:  [218, 219, 220]           # Ground truth

Precision@1 = 1/1 = 1.0   # Doc #1 es relevante ✅
Precision@3 = 2/3 = 0.67  # 2 de 3 son relevantes
Precision@5 = 3/5 = 0.60  # 3 de 5 son relevantes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Recall@k&lt;/strong&gt;: ¿Qué % de documentos relevantes recuperamos?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recall@k = (Documentos relevantes en top-k) / Total relevantes

Ejemplo:
Recall@5 = 3/3 = 1.0  # ✅ Recuperamos TODOS los relevantes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. MRR (Mean Reciprocal Rank)&lt;/strong&gt;: Posición promedio del primer resultado relevante&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MRR = 1 / (Posición del primer relevante)

Query 1: Relevante en #1 → MRR = 1/1 = 1.0
Query 2: Relevante en #3 → MRR = 1/3 = 0.33
Promedio: 0.67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cómo implementarlas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crear un &lt;strong&gt;ground truth dataset&lt;/strong&gt;: 50-100 queries con documentos relevantes anotados manualmente&lt;/li&gt;
&lt;li&gt;Ejecutar queries contra Qdrant&lt;/li&gt;
&lt;li&gt;Comparar resultados vs ground truth&lt;/li&gt;
&lt;li&gt;Calcular métricas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;📊 Evaluación pre-producción del proyecto&lt;/strong&gt;: Antes de implementar el sistema en producción, se realizaron evaluaciones exhaustivas documentadas en:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/notebooks/02_vectorstore_embedding_exploration.ipynb" rel="noopener noreferrer"&gt;&lt;code&gt;02_vectorstore_embedding_exploration.ipynb&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Evaluación de 11 modelos de embeddings&lt;/li&gt;
&lt;li&gt;Métricas de performance y calidad&lt;/li&gt;
&lt;li&gt;Justificación del modelo seleccionado (&lt;code&gt;multilingual-e5-small&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/notebooks/03_rag_pipeline_evaluation.ipynb" rel="noopener noreferrer"&gt;&lt;code&gt;03_rag_pipeline_evaluation.ipynb&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ground truth dataset&lt;/strong&gt; de 50+ queries legales&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Métricas de retrieval&lt;/strong&gt;: Precision@k, Recall@k, MRR, nDCG@k, Hit Rate@k&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluación de reranking&lt;/strong&gt;: Comparación antes/después con cross-encoder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluación LLM&lt;/strong&gt;: LLM-as-a-Judge para medir calidad de respuestas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Análisis comparativo&lt;/strong&gt;: Múltiples configuraciones del pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Estos notebooks representan la &lt;strong&gt;evidencia empírica&lt;/strong&gt; que respaldó las decisiones técnicas del sistema RAG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Herramientas recomendadas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAGAS&lt;/strong&gt;: Framework de evaluación de RAG (&lt;a href="https://github.com/explodinggradients/ragas" rel="noopener noreferrer"&gt;github.com/explodinggradients/ragas&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix Evals&lt;/strong&gt;: Incluye retrieval metrics (&lt;a href="https://docs.arize.com/phoenix" rel="noopener noreferrer"&gt;docs.arize.com/phoenix&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Integración en el RAG Pipeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;En el RAGService&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&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;session_id&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Complete RAG pipeline with evaluation&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Retrieve documents
&lt;/span&gt;    &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_retrieve_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Generate answer
&lt;/span&gt;    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_generate_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Build context
&lt;/span&gt;    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Enqueue evaluation (NON-BLOCKING)
&lt;/span&gt;    &lt;span class="n"&gt;evaluation_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;metadata&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;llm_provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;processing_time&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;span class="c1"&gt;# 5. Return response immediately (evaluation runs in background)
&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;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Timeline:&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;t=0ms:    User request arrives
t=100ms:  Documents retrieved
t=2000ms: LLM generates answer
t=2010ms: Response returned to user ← USER HAPPY
t=2050ms: Evaluation enqueued (async)
t=4500ms: Evaluations complete (background) ← METRICS TRACKED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User experience&lt;/strong&gt;: 2 seconds (no ve los 2.5s de evaluación)&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;Visualización en Phoenix UI&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Dashboard de Métricas&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Acceso: &lt;code&gt;http://localhost:6006&lt;/code&gt; (local) o Phoenix Cloud&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vista de Evaluaciones:&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;┌─────────────────────────────────────────────────────┐
│ Evaluation Results (Last 7 days)                   │
│                                                     │
│ Total Queries Evaluated: 1,234                     │
│                                                     │
│ Relevance (avg):      0.89 ████████████████░░ 89%  │
│ Hallucination (avg):  0.05 ░░░░░░░░░░░░░░░░░░  5%  │
│ Toxicity (avg):       0.01 ░░░░░░░░░░░░░░░░░░  1%  │
│ Grounding (avg):      0.95 █████████████████░ 95%  │
│                                                     │
│ ┌─────────────────────────────────────────────┐   │
│ │  Relevance Distribution                     │   │
│ │  ┌────────────────────────────────────┐    │   │
│ │  │ [0.0-0.2]:  ▓ 2%                    │    │   │
│ │  │ [0.2-0.4]:  ▓ 3%                    │    │   │
│ │  │ [0.4-0.6]:  ▓▓ 5%                   │    │   │
│ │  │ [0.6-0.8]:  ▓▓▓▓▓ 12%               │    │   │
│ │  │ [0.8-1.0]:  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 78%     │    │   │
│ │  └────────────────────────────────────┘    │   │
│ └─────────────────────────────────────────────┘   │
│                                                     │
│ Top Issues:                                         │
│ - 12 queries with low relevance (&amp;lt;0.6)             │
│ - 6 potential hallucinations detected (&amp;gt;0.3)       │
│ - 0 toxic responses                                │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Drill-Down Individual&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│ Session: abc-123-def                                │
│ Timestamp: 2024-10-16 14:30:22                      │
│                                                     │
│ Question:                                           │
│ "¿Cuántos días de vacaciones corresponden?"        │
│                                                     │
│ Answer:                                             │
│ "Según el Artículo 218 del Código del Trabajo..."  │
│                                                     │
│ Evaluations:                                        │
│ ├─ Relevance:      0.92 ✅ (Highly relevant)       │
│ ├─ Hallucination:  0.03 ✅ (Minimal)               │
│ ├─ Toxicity:       0.00 ✅ (None)                  │
│ └─ Grounding:      0.97 ✅ (Well grounded)         │
│                                                     │
│ Retrieved Documents:                                │
│ 1. Art. 218 (score: 0.912, rerank: 0.987)         │
│ 2. Art. 219 (score: 0.876, rerank: 0.921)         │
│ 3. Art. 220 (score: 0.845, rerank: 0.889)         │
│                                                     │
│ Metadata:                                           │
│ - LLM: OpenAI GPT-3.5-turbo                        │
│ - Processing time: 1.234s                           │
│ - Reranking: Enabled                                │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Casos de Uso Reales&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Identificar Problemas de Calidad:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Algunas respuestas parecen incorrectas, pero no sé cuáles"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abrir Phoenix UI&lt;/li&gt;
&lt;li&gt;Filtrar por &lt;code&gt;relevance &amp;lt; 0.6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ver queries problemáticas:

&lt;ul&gt;
&lt;li&gt;"¿Qué es un contrato eventual?" → Relevance: 0.4&lt;/li&gt;
&lt;li&gt;Contexto recuperado: artículos sobre "contratos colectivos"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problema&lt;/strong&gt;: Embeddings confunden "eventual" con "colectivo"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Mejorar pre-processing de queries o usar modelo de embeddings más grande&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Detectar Hallucinations:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"¿El LLM está inventando información?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Phoenix UI → Filter &lt;code&gt;hallucination &amp;gt; 0.3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ver casos:

&lt;ul&gt;
&lt;li&gt;Query: "¿Cuál es el salario mínimo?"&lt;/li&gt;
&lt;li&gt;Context: (vacío - no hay artículos sobre salario)&lt;/li&gt;
&lt;li&gt;Answer: "El salario mínimo es $2,500,000..." ⚠️&lt;/li&gt;
&lt;li&gt;Hallucination score: 0.9&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Agregar guardrail: Si no hay contexto relevante → "No tengo información suficiente"&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Optimizar Performance:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"¿Qué configuración da mejores resultados?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución: A/B Testing&lt;/strong&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="c1"&gt;# Configuración A: Sin reranking
&lt;/span&gt;&lt;span class="n"&gt;results_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_eval_suite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;use_reranking&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="c1"&gt;# Relevance: 0.85
&lt;/span&gt;
&lt;span class="c1"&gt;# Configuración B: Con reranking
&lt;/span&gt;&lt;span class="n"&gt;results_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_eval_suite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;use_reranking&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="c1"&gt;# Relevance: 0.92 (+7%)
&lt;/span&gt;
&lt;span class="c1"&gt;# Conclusión: Reranking vale la pena!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Monitoreo Continuo:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero alertas cuando calidad baja"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&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="c1"&gt;# En Phoenix o monitoring system
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;avg_relevance_last_24h&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠️ RAG quality degraded! Avg relevance: 0.78&lt;/span&gt;&lt;span class="sh"&gt;"&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;hallucination_rate_last_24h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚨 High hallucination rate: 12%&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;h2&gt;
  
  
  🚀 &lt;strong&gt;El Impacto Transformador&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Antes de Evaluación Automática:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🤷 &lt;strong&gt;Quality unknown&lt;/strong&gt;: "Parece que funciona bien..."&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Reactive debugging&lt;/strong&gt;: Usuarios reportan problemas&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;No metrics&lt;/strong&gt;: Sin datos para optimizar&lt;/li&gt;
&lt;li&gt;🎲 &lt;strong&gt;Blind optimization&lt;/strong&gt;: Cambios sin medir impacto&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Manual testing&lt;/strong&gt;: Probar 10-20 queries manualmente&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Después de Evaluación Automática:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📊 &lt;strong&gt;Quality measured&lt;/strong&gt;: "89% relevance, 5% hallucination"&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;Proactive debugging&lt;/strong&gt;: Identificar problemas antes que usuarios&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Data-driven&lt;/strong&gt;: Optimizar basado en métricas reales&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;A/B testing&lt;/strong&gt;: Comparar configuraciones objetivamente&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Continuous evaluation&lt;/strong&gt;: 100% de queries evaluadas automáticamente&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas de Mejora:&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Sin Evaluación&lt;/th&gt;
&lt;th&gt;Con Evaluación&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quality visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;+∞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Problem detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Días (user reports)&lt;/td&gt;
&lt;td&gt;Minutos (automated)&lt;/td&gt;
&lt;td&gt;-99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evaluation coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-5% (manual)&lt;/td&gt;
&lt;td&gt;100% (automated)&lt;/td&gt;
&lt;td&gt;+2000%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimization confidence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Baja (guessing)&lt;/td&gt;
&lt;td&gt;Alta (data-driven)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to fix issues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Días&lt;/td&gt;
&lt;td&gt;Horas&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;Lecciones Aprendidas&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Async Evaluation es Crítico&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Evaluación no debe agregar latencia a la respuesta del usuario. Queue + ThreadPoolExecutor = perfecto.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. gpt-4o-mini es Ideal para Evals&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPT-4&lt;/strong&gt;: Muy caro para evaluar cada query ($30/1M tokens)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-3.5-turbo&lt;/strong&gt;: Calidad inconsistente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gpt-4o-mini&lt;/strong&gt;: Balance perfecto ($0.15/1M tokens, buena calidad)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Phoenix Templates &amp;gt; Custom&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Los templates de Phoenix (HALLUCINATION, RAG_RELEVANCY) están battle-tested. Úsalos.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Parallel Evaluations = 3x Faster&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;asyncio.gather&lt;/code&gt; ejecuta 3 evals en paralelo → 2.5s en lugar de 7.5s&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Ground Truth es Gold&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Tener un dataset de 50-100 query-answer pairs validados permite comparaciones objetivas.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. Monitorea Tendencias, No Puntos&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Un query malo no importa. 10% de queries malos sí importa. Phoenix dashboards muestran trends.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Propósito Más Grande&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Evaluación y métricas no son "nice to have" - son el &lt;strong&gt;sistema de calidad&lt;/strong&gt; que garantiza que el RAG entrega valor real. Sin evaluación:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No sabes si está funcionando&lt;/li&gt;
&lt;li&gt;No puedes optimizar&lt;/li&gt;
&lt;li&gt;No detectas regresiones&lt;/li&gt;
&lt;li&gt;No justificas inversión&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con evaluación automática continua:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Visibility&lt;/strong&gt;: Calidad medida en cada query&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Confidence&lt;/strong&gt;: Cambios basados en datos&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Quality assurance&lt;/strong&gt;: Detección temprana de problemas&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Continuous improvement&lt;/strong&gt;: Optimización basada en métricas&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Stakeholder reporting&lt;/strong&gt;: Dashboards para el negocio&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cost optimization&lt;/strong&gt;: Identificar oportunidades de ahorro&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estamos convirtiendo el RAG de una "caja negra" a un &lt;strong&gt;sistema medido, monitoreado y optimizable&lt;/strong&gt;, donde cada respuesta contribuye a mejorar el siguiente millón de respuestas.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Recursos y Enlaces&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Repositorio del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Documentación Técnica&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation Service&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/services/evaluation_service.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/services/evaluation_service.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix Service&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/services/phoenix_service.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/services/phoenix_service.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG Service&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/services/rag_service.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/services/rag_service.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Recursos Externos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix Evals&lt;/strong&gt;: &lt;a href="https://docs.arize.com/phoenix/evaluation/llm-evals" rel="noopener noreferrer"&gt;docs.arize.com/phoenix/evaluation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG Evaluation Paper&lt;/strong&gt;: &lt;a href="https://arxiv.org/abs/2312.10997" rel="noopener noreferrer"&gt;arxiv.org/abs/2312.10997&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAGAS Framework&lt;/strong&gt;: &lt;a href="https://github.com/explodinggradients/ragas" rel="noopener noreferrer"&gt;github.com/explodinggradients/ragas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluating RAG Systems&lt;/strong&gt;: &lt;a href="https://arize.com/blog-course/rag-evaluation-metrics/" rel="noopener noreferrer"&gt;arize.com/blog/rag-evaluation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎊 &lt;strong&gt;Fin de la Serie LLPY&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Hemos completado un viaje de 14 posts construyendo un sistema RAG de clase mundial:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Serie Completa:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-01&lt;/strong&gt;: Introducción al Sistema RAG&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-02&lt;/strong&gt;: Configuración con UV&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-03&lt;/strong&gt;: Extracción de Datos Legales&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-04&lt;/strong&gt;: Vectorización y Embeddings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-05&lt;/strong&gt;: Qdrant Base de Datos Vectorial&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-06&lt;/strong&gt;: FastAPI API REST Robusta&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-07&lt;/strong&gt;: Integración LLMs (OpenAI + Gemini)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-08&lt;/strong&gt;: Reranking para Precisión&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-09&lt;/strong&gt;: Phoenix y OpenTelemetry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-10&lt;/strong&gt;: Autenticación JWT con RSA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-11&lt;/strong&gt;: Terraform - Infraestructura como Código&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-12&lt;/strong&gt;: Docker y Containerización&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-13&lt;/strong&gt;: CI/CD con GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLPY-14&lt;/strong&gt;: Evaluación y Métricas de Calidad ← &lt;strong&gt;ESTE POST&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lo que Construimos:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;✅ Sistema RAG completo end-to-end&lt;br&gt;&lt;br&gt;
✅ 413 artículos legales vectorizados&lt;br&gt;&lt;br&gt;
✅ API REST producción-ready&lt;br&gt;&lt;br&gt;
✅ Multi-provider LLM (OpenAI + Gemini)&lt;br&gt;&lt;br&gt;
✅ Observabilidad completa (Phoenix + OpenTelemetry)&lt;br&gt;&lt;br&gt;
✅ Infraestructura como código (Terraform)&lt;br&gt;&lt;br&gt;
✅ CI/CD automatizado (GitHub Actions)&lt;br&gt;&lt;br&gt;
✅ Evaluación continua (LLM-as-a-judge)  &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas del Sistema (Observables con Phoenix):&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Objetivo&lt;/th&gt;
&lt;th&gt;Medición&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency (p50)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;2s&lt;/td&gt;
&lt;td&gt;✅ Phoenix tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency (p95)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;5s&lt;/td&gt;
&lt;td&gt;✅ Phoenix tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Relevance (avg)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;0.8&lt;/td&gt;
&lt;td&gt;✅ LLM-as-a-Judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hallucination (avg)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;0.1&lt;/td&gt;
&lt;td&gt;✅ LLM-as-a-Judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Toxicity (avg)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;0.05&lt;/td&gt;
&lt;td&gt;✅ LLM-as-a-Judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overall Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;0.85&lt;/td&gt;
&lt;td&gt;✅ Weighted average&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost per query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;$0.01&lt;/td&gt;
&lt;td&gt;✅ Token tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nota&lt;/strong&gt;: Las métricas se recopilan automáticamente en Phoenix con cada query evaluada. Los valores "Objetivo" representan benchmarks típicos de RAG de producción, no mediciones actuales.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Impacto Transformador Final:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;De &lt;strong&gt;0 a sistema RAG de producción&lt;/strong&gt; en 14 posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Performance&lt;/strong&gt;: Retrieval optimizado (gRPC), generación async&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Quality&lt;/strong&gt;: LLM-as-a-Judge evaluando relevance, hallucination, toxicity&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost&lt;/strong&gt;: gpt-4o-mini para evals ($0.15/1M tokens), escalable&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Security&lt;/strong&gt;: JWT RSA auth, GCP Secret Manager&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Observability&lt;/strong&gt;: Phoenix + OpenTelemetry tracking completo&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Deployment&lt;/strong&gt;: CI/CD automático con GitHub Actions&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Evaluation&lt;/strong&gt;: 100% de queries evaluadas asíncronamente (non-blocking)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gracias por acompañarnos en este viaje!&lt;/strong&gt; 🎉&lt;/p&gt;

&lt;p&gt;Si tienes preguntas o quieres contribuir al proyecto, visita el repositorio en GitHub.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
    </item>
    <item>
      <title>LLPY-13: CI/CD con GitHub Actions - Automatización Completa</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Fri, 17 Oct 2025 18:29:09 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llpy-13-cicd-con-github-actions-automatizacion-completa-ij6</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llpy-13-cicd-con-github-actions-automatizacion-completa-ij6</guid>
      <description>&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Desafío de los Deployments Manuales&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagina el proceso tradicional de deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Developer hace cambios → 10 min
2. Run tests localmente → 5 min
3. Fix issues → 15 min
4. Build Docker image → 5 min
5. Push a Docker Hub → 3 min
6. SSH a servidor → 2 min
7. Pull nueva imagen → 2 min
8. Restart servicio → 1 min
9. Verificar deployment → 3 min
10. Rollback si falla → 10 min

TOTAL: 56 minutos + posibilidad de error humano
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problemas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Tiempo desperdiciado&lt;/strong&gt;: 1 hora por deploy&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Errores humanos&lt;/strong&gt;: Olvidar un paso, typo en comando&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Sin tracking&lt;/strong&gt;: ¿Quién deployó qué, cuándo?&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Inconsistencia&lt;/strong&gt;: Diferentes procesos por developer&lt;/li&gt;
&lt;li&gt;🚨 &lt;strong&gt;Sin rollback fácil&lt;/strong&gt;: Revertir requiere proceso manual&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;La Magnitud del Problema&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Requisitos de CI/CD Moderno&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;✅ Quality Gates&lt;/strong&gt;: Tests, linting, security scan automáticos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🏗️ Build Automation&lt;/strong&gt;: Docker images con cada cambio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📦 Artifact Management&lt;/strong&gt;: Versionado y storage de images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🚀 Auto Deployment&lt;/strong&gt;: Deploy a staging/prod automático&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔍 Validation&lt;/strong&gt;: Health checks post-deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 Notifications&lt;/strong&gt;: Alertas de successes/failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Rollback&lt;/strong&gt;: Fácil volver a versión anterior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔐 Secrets Management&lt;/strong&gt;: API keys, credentials seguros&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;7 Workflows del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Propósito&lt;/th&gt;
&lt;th&gt;Duración&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;code-quality.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push/PR&lt;/td&gt;
&lt;td&gt;Tests, linting, security&lt;/td&gt;
&lt;td&gt;3-5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;docker-processing-build-publish.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push/Manual&lt;/td&gt;
&lt;td&gt;Build processing image&lt;/td&gt;
&lt;td&gt;4-6 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;docker-api-build-publish.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push/Manual&lt;/td&gt;
&lt;td&gt;Build API image&lt;/td&gt;
&lt;td&gt;5-7 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;terraform-apply-on-tf-change.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Push .tf files&lt;/td&gt;
&lt;td&gt;Apply infra changes&lt;/td&gt;
&lt;td&gt;3-8 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;deploy-qdrant.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual/Push&lt;/td&gt;
&lt;td&gt;Deploy Qdrant to VM&lt;/td&gt;
&lt;td&gt;5-10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;update-batch-job-image.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Update batch job image&lt;/td&gt;
&lt;td&gt;2-3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;update-api-secrets-deploy.yml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Update secrets + deploy API&lt;/td&gt;
&lt;td&gt;3-5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;La Solución: GitHub Actions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es GitHub Actions?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; es una plataforma de CI/CD integrada en GitHub que permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🤖 &lt;strong&gt;Automatización&lt;/strong&gt;: Workflows como código (YAML)&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Trigger flexible&lt;/strong&gt;: Push, PR, schedule, manual&lt;/li&gt;
&lt;li&gt;🔧 &lt;strong&gt;Actions marketplace&lt;/strong&gt;: Miles de actions reutilizables&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;Secrets management&lt;/strong&gt;: Secrets encriptados integrados&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Logging&lt;/strong&gt;: Logs detallados de cada ejecución&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Free tier&lt;/strong&gt;: 2000 minutos/mes para repos públicos&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Arquitectura de un Workflow&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mi Workflow&lt;/span&gt;               &lt;span class="c1"&gt;# Nombre del workflow&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# Triggers&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                           &lt;span class="c1"&gt;# Jobs (paralelos por defecto)&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                        &lt;span class="c1"&gt;# Job 1&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;      &lt;span class="c1"&gt;# Runner&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# Steps (secuenciales)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;    &lt;span class="c1"&gt;# Action del marketplace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Hello"&lt;/span&gt;            &lt;span class="c1"&gt;# Comando shell&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t app .&lt;/span&gt;   &lt;span class="c1"&gt;# Step con nombre&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Workflow 1: Code Quality &amp;amp; Style&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/code-quality.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Code Quality &amp;amp; Style&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utils/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tests/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.pre-commit-config.yaml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pyproject.toml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.md'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.yaml'&lt;/span&gt;

  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utils/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tests/**/*.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.pre-commit-config.yaml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pyproject.toml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.md'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.yaml'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Job 1: Pre-commit hooks&lt;/span&gt;
  &lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pre-Commit Hooks&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install UV&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install pre-commit&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv tool install pre-commit&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache pre-commit hooks&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.cache/pre-commit&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run pre-commit hooks&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv tool run pre-commit run --all-files --show-diff-on-failure&lt;/span&gt;

  &lt;span class="c1"&gt;# Job 2: Lint API code&lt;/span&gt;
  &lt;span class="na"&gt;lint-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint API Code&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install UV&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install ruff&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv tool install ruff&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run ruff linter&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv tool run ruff check src/lus_laboris_api --output-format=github&lt;/span&gt;

  &lt;span class="c1"&gt;# Job 3: Security scan&lt;/span&gt;
  &lt;span class="na"&gt;security-scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security Scan (Bandit)&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install UV&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install bandit&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv tool install bandit&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run bandit security scan&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv tool run bandit -r src/ utils/ -f github&lt;/span&gt;

  &lt;span class="c1"&gt;# Job 4: Type checking&lt;/span&gt;
  &lt;span class="na"&gt;type-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Type Checking (MyPy)&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install UV&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd src/lus_laboris_api&lt;/span&gt;
        &lt;span class="s"&gt;uv sync&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run mypy&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd src/lus_laboris_api&lt;/span&gt;
        &lt;span class="s"&gt;uv run mypy api/ --ignore-missing-imports&lt;/span&gt;

  &lt;span class="c1"&gt;# Job 5: Tests&lt;/span&gt;
  &lt;span class="na"&gt;test-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API Tests (PyTest)&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.13'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install UV&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;astral-sh/setup-uv@v6&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd src/lus_laboris_api&lt;/span&gt;
        &lt;span class="s"&gt;uv sync&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests with coverage&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;cd src/lus_laboris_api&lt;/span&gt;
        &lt;span class="s"&gt;uv run pytest tests/ --cov=api --cov-report=term --cov-report=html&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload coverage reports&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage-report&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/lus_laboris_api/htmlcov/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quality Gates:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Pre-commit&lt;/strong&gt;: Formatting, trailing whitespace, YAML syntax&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Linting&lt;/strong&gt;: Ruff (10-100x más rápido que flake8)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Security&lt;/strong&gt;: Bandit detecta vulnerabilidades&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Type safety&lt;/strong&gt;: MyPy valida type hints&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Tests&lt;/strong&gt;: PyTest con coverage report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Triggers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push a cualquier branch que toque Python files&lt;/li&gt;
&lt;li&gt;Pull requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resultado&lt;/strong&gt;: Si algún job falla → PR bloqueado → Fix required&lt;/p&gt;

&lt;h2&gt;
  
  
  🐳 &lt;strong&gt;Workflows 2-3: Docker Build &amp;amp; Publish&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Workflow 2: Processing Image&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/docker-processing-build-publish.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Publish Docker Image (Processing)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/processing/Dockerfile'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/processing/extract_law_text.py'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/processing/pyproject.toml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/processing/.python-version'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Hub&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_USERNAME }}&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_PASSWORD }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get date tag&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date_tag&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "tag=$(date +'%Y%m%d')" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/processing&lt;/span&gt;
        &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-processing:latest&lt;/span&gt;
          &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-processing:${{ steps.date_tag.outputs.tag }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Workflow 3: API Image&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Similar a workflow 2, pero para &lt;code&gt;src/lus_laboris_api/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Automated tagging&lt;/strong&gt;: &lt;code&gt;latest&lt;/code&gt; + fecha (&lt;code&gt;20241016&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Trigger smart&lt;/strong&gt;: Solo rebuild si Dockerfile o código cambia&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Buildx&lt;/strong&gt;: Soporte multi-platform (AMD64, ARM64)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Registry caching&lt;/strong&gt;: Reutiliza layers de builds anteriores&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗️ &lt;strong&gt;Workflow 4: Terraform Apply&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/terraform-apply-on-tf-change.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply on TF Change&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terraform/**/*.tf'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terraform/**/*.tfvars'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcp-key.json&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Terraform&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;terraform_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.9.0&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create GCP credentials file&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;echo '${{ secrets.GSA_KEY }}' &amp;gt; gcp-key.json&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create terraform.tfvars&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;# Generate tfvars from GitHub secrets/variables&lt;/span&gt;
        &lt;span class="s"&gt;cat &amp;gt; terraform.tfvars &amp;lt;&amp;lt;EOF&lt;/span&gt;
        &lt;span class="s"&gt;project_id = "${{ secrets.GCP_PROJECT_ID }}"&lt;/span&gt;
        &lt;span class="s"&gt;region = "${{ secrets.GCP_REGION }}"&lt;/span&gt;
        &lt;span class="s"&gt;bucket_name = "${{ vars.GCP_BUCKET_NAME }}"&lt;/span&gt;
        &lt;span class="s"&gt;# ... (más variables)&lt;/span&gt;
        &lt;span class="s"&gt;EOF&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Validate&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform validate&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -out=tfplan&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply -auto-approve tfplan&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Show Outputs&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform output -json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nota sobre el workflow real&lt;/strong&gt;: El proyecto simplifica este paso usando el script &lt;code&gt;tf_menu.sh&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El workflow genera un &lt;code&gt;.env&lt;/code&gt; file con todas las variables de GitHub&lt;/li&gt;
&lt;li&gt;Luego ejecuta &lt;code&gt;bash tf_menu.sh 2&lt;/code&gt; que auto-genera &lt;code&gt;terraform.tfvars&lt;/code&gt; desde &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Este enfoque reutiliza la misma lógica que el desarrollo local&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Triggers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cambios en cualquier archivo &lt;code&gt;.tf&lt;/code&gt; (no &lt;code&gt;.tfvars&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Manual via workflow_dispatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Proceso:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Checkout código&lt;/li&gt;
&lt;li&gt;✅ Authenticate to GCP&lt;/li&gt;
&lt;li&gt;✅ Setup Terraform&lt;/li&gt;
&lt;li&gt;✅ Generate .env file con variables de GitHub secrets/vars&lt;/li&gt;
&lt;li&gt;✅ Run &lt;code&gt;tf_menu.sh 2&lt;/code&gt; para crear terraform.tfvars&lt;/li&gt;
&lt;li&gt;✅ Init → Plan → Apply (auto-approve)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Resultado&lt;/strong&gt;: Infraestructura actualizada automáticamente en 3-8 minutos&lt;/p&gt;

&lt;h2&gt;
  
  
  🗄️ &lt;strong&gt;Workflow 5: Deploy Qdrant to VM&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/deploy-qdrant.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Qdrant to GCP VM&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;services/vectordb/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terraform/modules/compute_engine/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authenticate to GCP&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;credentials_json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GSA_KEY }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Cloud SDK&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/setup-gcloud@v2&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get VM details&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vm_info&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;VM_EXTERNAL_IP=$(gcloud compute instances describe ${{ vars.GCP_COMPUTE_ENGINE_VM_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--zone=${{ vars.GCP_COMPUTE_ENGINE_VM_ZONE }} \&lt;/span&gt;
          &lt;span class="s"&gt;--format='get(networkInterfaces[0].accessConfigs[0].natIP)')&lt;/span&gt;

        &lt;span class="s"&gt;echo "vm_ip=${VM_EXTERNAL_IP}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Docker on VM&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gcloud compute ssh ${{ vars.GCP_COMPUTE_ENGINE_VM_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--zone=${{ vars.GCP_COMPUTE_ENGINE_VM_ZONE }} \&lt;/span&gt;
          &lt;span class="s"&gt;--command="&lt;/span&gt;
            &lt;span class="s"&gt;# Install Docker if not exists&lt;/span&gt;
            &lt;span class="s"&gt;if ! command -v docker &amp;amp;&amp;gt; /dev/null; then&lt;/span&gt;
              &lt;span class="s"&gt;curl -fsSL https://get.docker.com -o get-docker.sh&lt;/span&gt;
              &lt;span class="s"&gt;sudo sh get-docker.sh&lt;/span&gt;
              &lt;span class="s"&gt;sudo usermod -aG docker \$USER&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;

            &lt;span class="s"&gt;# Install Docker Compose&lt;/span&gt;
            &lt;span class="s"&gt;sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-\$(uname -s)-\$(uname -m) \&lt;/span&gt;
              &lt;span class="s"&gt;-o /usr/local/bin/docker-compose&lt;/span&gt;
            &lt;span class="s"&gt;sudo chmod +x /usr/local/bin/docker-compose&lt;/span&gt;
          &lt;span class="s"&gt;"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Qdrant&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;# Copy docker-compose.yml&lt;/span&gt;
        &lt;span class="s"&gt;gcloud compute scp services/vectordb/docker-compose.yml \&lt;/span&gt;
          &lt;span class="s"&gt;${{ vars.GCP_COMPUTE_ENGINE_VM_NAME }}:~/docker-compose.yml \&lt;/span&gt;
          &lt;span class="s"&gt;--zone=${{ vars.GCP_COMPUTE_ENGINE_VM_ZONE }}&lt;/span&gt;

        &lt;span class="s"&gt;# Create .env with API key&lt;/span&gt;
        &lt;span class="s"&gt;gcloud compute ssh ${{ vars.GCP_COMPUTE_ENGINE_VM_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--zone=${{ vars.GCP_COMPUTE_ENGINE_VM_ZONE }} \&lt;/span&gt;
          &lt;span class="s"&gt;--command="&lt;/span&gt;
            &lt;span class="s"&gt;echo 'QDRANT_API_KEY=${{ secrets.QDRANT_API_KEY }}' &amp;gt; ~/.env&lt;/span&gt;

            &lt;span class="s"&gt;# Start Qdrant&lt;/span&gt;
            &lt;span class="s"&gt;docker-compose up -d&lt;/span&gt;

            &lt;span class="s"&gt;# Wait and verify&lt;/span&gt;
            &lt;span class="s"&gt;sleep 10&lt;/span&gt;
            &lt;span class="s"&gt;curl -f http://localhost:6333/healthz || exit 1&lt;/span&gt;
          &lt;span class="s"&gt;"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify deployment&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;curl -f http://${{ steps.vm_info.outputs.vm_ip }}:6333/collections&lt;/span&gt;
        &lt;span class="s"&gt;echo "✅ Qdrant deployed successfully at ${{ steps.vm_info.outputs.vm_ip }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nota&lt;/strong&gt;: El workflow real (130 líneas) es &lt;strong&gt;mucho más robusto&lt;/strong&gt; que este ejemplo simplificado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Wait for VM ready&lt;/strong&gt;: 30 retries con 10s entre cada uno&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Create separate directory&lt;/strong&gt;: Crea ~/qdrant antes de .env&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Robust verification&lt;/strong&gt;: 30 attempts con 15s sleeps para health check&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Connection info&lt;/strong&gt;: Muestra API URL + Dashboard URL al final&lt;/li&gt;
&lt;li&gt;Simplificado aquí para fines educativos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Proceso:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get VM external IP&lt;/li&gt;
&lt;li&gt;Wait for VM to be SSH-ready (30 retries)&lt;/li&gt;
&lt;li&gt;Install Docker + Docker Compose (si no existe)&lt;/li&gt;
&lt;li&gt;Create qdrant directory y .env with API key&lt;/li&gt;
&lt;li&gt;Copy docker-compose.yml&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify health check (30 retries, 15s cada uno)&lt;/li&gt;
&lt;li&gt;Display connection info (IP, API URL, Dashboard)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Resultado&lt;/strong&gt;: Qdrant running en VM con persistencia en 5-10 minutos&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Workflow 6: Update Batch Job Image&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/update-batch-job-image.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Batch Job Image&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update-job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authenticate to GCP&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;credentials_json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GSA_KEY }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Cloud SDK&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/setup-gcloud@v2&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Cloud Run Job image&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;IMAGE="${{ secrets.DOCKER_HUB_USERNAME }}/${{ vars.DOCKER_IMAGE_NAME_PROCESSING }}:${{ vars.GCP_CLOUD_RUN_BATCH_IMAGE_TAG }}"&lt;/span&gt;

        &lt;span class="s"&gt;echo "🔄 Updating job image to: ${IMAGE}"&lt;/span&gt;

        &lt;span class="s"&gt;gcloud run jobs update ${{ vars.GCP_CLOUD_RUN_BATCH_JOB_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--image=${IMAGE} \&lt;/span&gt;
          &lt;span class="s"&gt;--region=${{ secrets.GCP_REGION }} \&lt;/span&gt;
          &lt;span class="s"&gt;--project=${{ secrets.GCP_PROJECT_ID }}&lt;/span&gt;

        &lt;span class="s"&gt;echo "✅ Job updated successfully!"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify update&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gcloud run jobs describe ${{ vars.GCP_CLOUD_RUN_BATCH_JOB_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--region=${{ secrets.GCP_REGION }} \&lt;/span&gt;
          &lt;span class="s"&gt;--project=${{ secrets.GCP_PROJECT_ID }} \&lt;/span&gt;
          &lt;span class="s"&gt;--format='value(template.template.containers[0].image)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nota&lt;/strong&gt;: El workflow real (128 líneas) tiene &lt;strong&gt;validación y debugging avanzados&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Args expansion&lt;/strong&gt;: Expande &lt;code&gt;${GCP_BUCKET_NAME}&lt;/code&gt; con sed&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Format conversion&lt;/strong&gt;: Space-separated → comma-separated args&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Error handling&lt;/strong&gt;: &lt;code&gt;job describe&lt;/code&gt; si update falla&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Full verification&lt;/strong&gt;: Usa &lt;code&gt;jq&lt;/code&gt; para parsear y verificar image + args&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Schedule display&lt;/strong&gt;: Muestra próxima ejecución programada&lt;/li&gt;
&lt;li&gt;Simplificado aquí para fines educativos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Uso:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build nueva imagen → Push a Docker Hub (workflow 2)&lt;/li&gt;
&lt;li&gt;Cambiar variable &lt;code&gt;GCP_CLOUD_RUN_BATCH_IMAGE_TAG&lt;/code&gt; a nuevo tag&lt;/li&gt;
&lt;li&gt;Ejecutar workflow 6 manualmente&lt;/li&gt;
&lt;li&gt;Job actualizado y verificado en 2-3 minutos&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Workflow 7: Update API Secrets &amp;amp; Deploy&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/update-api-secrets-deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update API Secrets &amp;amp; Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Authenticate to GCP&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;credentials_json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GSA_KEY }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Cloud SDK&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/setup-gcloud@v2&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Secrets in Secret Manager&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.GCP_CLOUD_SECRETS_UPDATE == 'true' }}&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;echo "📝 Updating secrets..."&lt;/span&gt;

        &lt;span class="s"&gt;# Update .env secret&lt;/span&gt;
        &lt;span class="s"&gt;echo "${{ secrets.API_ENV_FILE }}" | \&lt;/span&gt;
          &lt;span class="s"&gt;gcloud secrets versions add ${{ vars.GCP_CLOUD_SECRETS_API_ENV_ID }} \&lt;/span&gt;
          &lt;span class="s"&gt;--data-file=- \&lt;/span&gt;
          &lt;span class="s"&gt;--project=${{ secrets.GCP_PROJECT_ID }}&lt;/span&gt;

        &lt;span class="s"&gt;# Update JWT public key&lt;/span&gt;
        &lt;span class="s"&gt;echo "${{ secrets.JWT_PUBLIC_KEY }}" | \&lt;/span&gt;
          &lt;span class="s"&gt;gcloud secrets versions add ${{ vars.GCP_CLOUD_SECRETS_JWT_KEY_ID }} \&lt;/span&gt;
          &lt;span class="s"&gt;--data-file=- \&lt;/span&gt;
          &lt;span class="s"&gt;--project=${{ secrets.GCP_PROJECT_ID }}&lt;/span&gt;

        &lt;span class="s"&gt;echo "✅ Secrets updated"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy API to Cloud Run&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;IMAGE="${{ secrets.DOCKER_HUB_USERNAME }}/${{ vars.DOCKER_IMAGE_NAME_RAG_API }}:${{ vars.GCP_CLOUD_RUN_API_IMAGE_TAG }}"&lt;/span&gt;

        &lt;span class="s"&gt;echo "🚀 Deploying ${IMAGE}..."&lt;/span&gt;

        &lt;span class="s"&gt;gcloud run deploy ${{ vars.GCP_CLOUD_RUN_API_SERVICE_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--image=${IMAGE} \&lt;/span&gt;
          &lt;span class="s"&gt;--region=${{ secrets.GCP_REGION }} \&lt;/span&gt;
          &lt;span class="s"&gt;--project=${{ secrets.GCP_PROJECT_ID }} \&lt;/span&gt;
          &lt;span class="s"&gt;--port=8000 \&lt;/span&gt;
          &lt;span class="s"&gt;--update-secrets="/app/secrets/env/.env=${{ vars.GCP_CLOUD_SECRETS_API_ENV_ID }}:latest,/app/secrets/jwt/public_key.pem=${{ vars.GCP_CLOUD_SECRETS_JWT_KEY_ID }}:latest" \&lt;/span&gt;
          &lt;span class="s"&gt;--cpu=${{ vars.GCP_CLOUD_RUN_API_CPU }} \&lt;/span&gt;
          &lt;span class="s"&gt;--memory=${{ vars.GCP_CLOUD_RUN_API_MEMORY }} \&lt;/span&gt;
          &lt;span class="s"&gt;--min-instances=${{ vars.GCP_CLOUD_RUN_API_MIN_INSTANCES }} \&lt;/span&gt;
          &lt;span class="s"&gt;--max-instances=${{ vars.GCP_CLOUD_RUN_API_MAX_INSTANCES }} \&lt;/span&gt;
          &lt;span class="s"&gt;--timeout=300 \&lt;/span&gt;
          &lt;span class="s"&gt;--allow-unauthenticated&lt;/span&gt;

        &lt;span class="s"&gt;echo "✅ API deployed!"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get Service URL&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;URL=$(gcloud run services describe ${{ vars.GCP_CLOUD_RUN_API_SERVICE_NAME }} \&lt;/span&gt;
          &lt;span class="s"&gt;--region=${{ secrets.GCP_REGION }} \&lt;/span&gt;
          &lt;span class="s"&gt;--format='value(status.url)' \&lt;/span&gt;
          &lt;span class="s"&gt;--project=${{ secrets.GCP_PROJECT_ID }})&lt;/span&gt;

        &lt;span class="s"&gt;echo "🌐 Service URL: ${URL}"&lt;/span&gt;
        &lt;span class="s"&gt;echo "📋 Health Check: ${URL}/api/health"&lt;/span&gt;
        &lt;span class="s"&gt;echo "📊 Swagger UI: ${URL}/docs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proceso completo:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;(Opcional) Update secrets en Secret Manager&lt;/li&gt;
&lt;li&gt;Deploy nueva versión de la imagen&lt;/li&gt;
&lt;li&gt;Cloud Run hace rolling update (zero downtime)&lt;/li&gt;
&lt;li&gt;Obtener URL del servicio&lt;/li&gt;
&lt;li&gt;Mostrar health check y Swagger endpoints&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Flujo Completo de CI/CD&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Escenario: Nuevo Feature&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer:
├─ 1. Crea branch: feature/add-reranking
├─ 2. Escribe código: reranking_service.py
├─ 3. Commit &amp;amp; push
│
GitHub Actions (automático):
├─ 4. [Workflow 1] Code Quality
│     ├─ Pre-commit ✅
│     ├─ Linting ✅
│     ├─ Security scan ✅
│     ├─ Type check ✅
│     └─ Tests ✅
│
Developer:
├─ 5. Create Pull Request
│
GitHub:
├─ 6. PR shows all checks passed ✅
│
Team:
├─ 7. Code review
├─ 8. Approve PR
├─ 9. Merge to main
│
GitHub Actions (automático):
├─ 10. [Workflow 3] Build API image
│      └─ Push: username/api:latest + :20241016
│
Developer:
├─ 11. Update variable: GCP_CLOUD_RUN_API_IMAGE_TAG=20241016
│
GitHub Actions (manual):
├─ 12. [Workflow 7] Deploy API
│      ├─ Update secrets (optional)
│      ├─ Deploy to Cloud Run
│      └─ Verify health check ✅
│
Production:
└─ 13. New feature live! 🎉

Total time: 15-20 minutos (automático)
Manual steps: 5 (branch, commit, PR, merge, trigger deploy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧪 &lt;strong&gt;Testing Local con Act&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es Act?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Act&lt;/strong&gt; permite ejecutar GitHub Actions workflows localmente con Docker:&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;# Instalar act&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;act  &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="c"&gt;# o&lt;/span&gt;
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Script Interactivo: act_menu.sh&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El proyecto incluye un &lt;strong&gt;script sofisticado de 310 líneas&lt;/strong&gt; para testing local de workflows con &lt;code&gt;act&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Características clave&lt;/strong&gt; (similar al &lt;code&gt;tf_menu.sh&lt;/code&gt; de LLPY-11):&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;# Features del script real:&lt;/span&gt;

1. VALIDACIÓN DE VARIABLES REQUERIDAS
   - Define required vars por workflow
   - Valida antes de ejecutar
   - Busca en .env automáticamente

2. SECRETS VS VARIABLES
   - Lista de vars que son secrets
   - Usa &lt;span class="nt"&gt;-s&lt;/span&gt; para secrets, &lt;span class="nt"&gt;--var&lt;/span&gt; para variables
   - Oculta valores sensibles en output

3. DOCKER HUB LOGIN AUTOMÁTICO
   - Login antes de ejecutar workflows
   - Previene errores de pull/push

4. GCP CREDENTIALS HANDLING
   - Busca automáticamente .json en .gcpcredentials/
   - Convierte a formato act-compatible

5. MANEJO ESPECIAL CODE-QUALITY
   - Ejecuta &lt;span class="nb"&gt;jobs &lt;/span&gt;secuencialmente &lt;span class="o"&gt;(&lt;/span&gt;pre-commit, lint, etc&lt;span class="o"&gt;)&lt;/span&gt;
   - Previene conflictos de act en paralelo
   - Opción de continuar si job falla

6. CONFIRMACIÓN INTERACTIVA
   - Muestra comando con valores ocultos
   - Pide confirmación antes de ejecutar

7. ERROR HANDLING
   - Valida .env existe
   - Valida GCP credentials
   - Mensajes de error claros
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ejemplo simplificado de uso:&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="nb"&gt;cd&lt;/span&gt; .github/workflows
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x act_menu.sh
./act_menu.sh

&lt;span class="c"&gt;# Menu interactivo:&lt;/span&gt;
&lt;span class="c"&gt;# 1) Code Quality &amp;amp; Style Check&lt;/span&gt;
&lt;span class="c"&gt;# 2) Build &amp;amp; Publish Docker Image (processing)&lt;/span&gt;
&lt;span class="c"&gt;# 3) Build &amp;amp; Publish Docker Image (API)&lt;/span&gt;
&lt;span class="c"&gt;# 4) Terraform Apply on .tf Change&lt;/span&gt;
&lt;span class="c"&gt;# 5) Deploy Qdrant to VM&lt;/span&gt;
&lt;span class="c"&gt;# 6) Update API Secrets &amp;amp; Deploy to Cloud Run&lt;/span&gt;
&lt;span class="c"&gt;# 7) Update Batch Job Image&lt;/span&gt;
&lt;span class="c"&gt;# 0) Salir&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Por qué es tan complejo:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;310 líneas&lt;/strong&gt;: Validación robusta de ~50 variables diferentes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets handling&lt;/strong&gt;: 10+ secrets que deben manejarse correctamente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP integration&lt;/strong&gt;: Búsqueda y formato de credentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User experience&lt;/strong&gt;: Mensajes claros, confirmaciones, error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas de Act:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Test locally&lt;/strong&gt;: Antes de push, verify workflow&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Faster iteration&lt;/strong&gt;: No wait for GitHub runners&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Debug&lt;/strong&gt;: Ver logs en tiempo real&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cost&lt;/strong&gt;: Zero (no consume GitHub Actions minutes)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Casos de Uso Reales&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Continuous Integration:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero asegurar calidad de código en cada PR"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workflow 1 (Code Quality) se ejecuta automáticamente&lt;/li&gt;
&lt;li&gt;PR no se puede mergear si hay fallos&lt;/li&gt;
&lt;li&gt;Code review con confianza de que quality checks pasaron&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Continuous Deployment:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero deploy automático a staging en cada merge a main"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Build image&lt;/span&gt;
      &lt;span class="c1"&gt;# Push to registry&lt;/span&gt;
      &lt;span class="c1"&gt;# Deploy to Cloud Run (staging environment)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Rollback Rápido:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"La última versión tiene un bug crítico"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# 1. Cambiar IMAGE_TAG a versión anterior&lt;/span&gt;
&lt;span class="nv"&gt;GCP_CLOUD_RUN_API_IMAGE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20241015  &lt;span class="c"&gt;# Era 20241016&lt;/span&gt;

&lt;span class="c"&gt;# 2. Ejecutar workflow 7&lt;/span&gt;
&lt;span class="c"&gt;# GitHub Actions → Deploy versión anterior&lt;/span&gt;

&lt;span class="c"&gt;# 3. Verificar&lt;/span&gt;
curl https://api-url/api/health

&lt;span class="c"&gt;# ✅ Rollback en 3 minutos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Multi-Environment:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero deploy a dev, staging, prod con diferentes configs"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Workflow con matrix strategy&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to ${{ matrix.environment }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gcloud run deploy api-${{ matrix.environment }} \&lt;/span&gt;
            &lt;span class="s"&gt;--image=... \&lt;/span&gt;
            &lt;span class="s"&gt;--set-env-vars="ENV=${{ matrix.environment }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;El Impacto Transformador&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Antes de GitHub Actions:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Deploy time&lt;/strong&gt;: 1 hora manual&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Error rate&lt;/strong&gt;: 20-30% (typos, pasos olvidados)&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Tracking&lt;/strong&gt;: Notas en Slack, memoria&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Rollback&lt;/strong&gt;: 30-60 minutos manual&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost&lt;/strong&gt;: Jenkins server ($50-100/mes)&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;Testing&lt;/strong&gt;: "Esperemos que funcione en prod"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Después de GitHub Actions:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Deploy time&lt;/strong&gt;: 15-20 minutos automático&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Error rate&lt;/strong&gt;: &amp;lt;5% (proceso automatizado)&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Tracking&lt;/strong&gt;: Cada deploy en Git + GitHub UI&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Rollback&lt;/strong&gt;: 3 minutos (cambiar tag + trigger)&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost&lt;/strong&gt;: $0 (free tier suficiente)&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;Testing&lt;/strong&gt;: Tests obligatorios antes de merge&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas de Mejora:&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Manual&lt;/th&gt;
&lt;th&gt;Con GitHub Actions&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deploy time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;60 min&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;td&gt;-75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error rate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;td&gt;-80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deploy frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-2/semana&lt;/td&gt;
&lt;td&gt;10-20/semana&lt;/td&gt;
&lt;td&gt;+900%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-60 min&lt;/td&gt;
&lt;td&gt;3 min&lt;/td&gt;
&lt;td&gt;-95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to production&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Días&lt;/td&gt;
&lt;td&gt;Horas&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;Lecciones Aprendidas&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Separate Workflows &amp;gt; Monolithic&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;7 workflows pequeños y específicos &amp;gt; 1 workflow gigante. Más fácil debug y maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Secrets vs Variables&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt;: API keys, passwords (encriptados)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables&lt;/strong&gt;: Config pública (service names, regions)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Manual Triggers son Poder&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; permite control fino sobre deployments críticos.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Path Filters Ahorran Minutos&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Solo ejecutar workflow si archivos relevantes cambiaron = menos tiempo desperdiciado.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Artifacts para Debugging&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;coverage-report&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;htmlcov/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Útil para ver coverage reports, logs, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. Act para Desarrollo de Workflows&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Desarrollar workflows con act localmente = 10x más rápido que push-wait-check en GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Propósito Más Grande&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;GitHub Actions no es solo CI/CD - es &lt;strong&gt;calidad automatizada&lt;/strong&gt;. Al automatizar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;✅ Quality&lt;/strong&gt;: Tests obligatorios en cada cambio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🏗️ Build&lt;/strong&gt;: Imágenes consistentes y versionadas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🚀 Deploy&lt;/strong&gt;: Proceso reproducible y traceable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔍 Visibility&lt;/strong&gt;: Cada ejecución logged y auditable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Speed&lt;/strong&gt;: De código a producción en minutos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🛡️ Safety&lt;/strong&gt;: Quality gates previenen broken deploys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💰 Cost&lt;/strong&gt;: Zero infrastructure, pay-per-use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;👥 Collaboration&lt;/strong&gt;: Workflows versionados = team aligned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estamos convirtiendo el deployment de un proceso manual propenso a errores en un &lt;strong&gt;pipeline automatizado confiable&lt;/strong&gt; que entrega valor a producción múltiples veces por día.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Recursos y Enlaces&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Repositorio del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Documentación Técnica&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflows Folder&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/tree/main/.github/workflows" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Quality&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/.github/workflows/code-quality.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/code-quality.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker API Build&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/.github/workflows/docker-api-build-publish.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/docker-api-build-publish.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Apply&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/.github/workflows/terraform-apply-on-tf-change.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/terraform-apply-on-tf-change.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Qdrant&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/.github/workflows/deploy-qdrant.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/deploy-qdrant.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update API&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/.github/workflows/update-api-secrets-deploy.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/update-api-secrets-deploy.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Recursos Externos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions Docs&lt;/strong&gt;: &lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;docs.github.com/actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act (Local Testing)&lt;/strong&gt;: &lt;a href="https://github.com/nektos/act" rel="noopener noreferrer"&gt;github.com/nektos/act&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions Marketplace&lt;/strong&gt;: &lt;a href="https://github.com/marketplace?type=actions" rel="noopener noreferrer"&gt;github.com/marketplace?type=actions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow Syntax&lt;/strong&gt;: &lt;a href="https://docs.github.com/actions/reference/workflow-syntax-for-github-actions" rel="noopener noreferrer"&gt;docs.github.com/actions/reference/workflow-syntax&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Próximo Post&lt;/strong&gt;: LLPY-14 - Evaluación y Métricas de Calidad&lt;/p&gt;

&lt;p&gt;En el siguiente y último post de la serie, exploraremos cómo evaluar y medir la calidad del sistema RAG end-to-end, incluyendo ground truth datasets, métricas de retrieval y generation, LLM-as-a-judge evaluations, y monitoreo continuo en producción.&lt;/p&gt;

</description>
      <category>github</category>
      <category>spanish</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>LLPY-12: Docker y Containerización - De Desarrollo a Producción</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Fri, 17 Oct 2025 18:28:26 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llpy-12-docker-y-containerizacion-de-desarrollo-a-produccion-1fpm</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llpy-12-docker-y-containerizacion-de-desarrollo-a-produccion-1fpm</guid>
      <description>&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Desafío de la Portabilidad y Reproducibilidad&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagina este escenario familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Tu código funciona &lt;strong&gt;perfecto en tu laptop&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;❌ Falla en el servidor de staging&lt;/li&gt;
&lt;li&gt;❌ Falla diferente en producción&lt;/li&gt;
&lt;li&gt;❌ Nuevo desarrollador tarda 2 días en setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;El problema&lt;/strong&gt;: &lt;em&gt;"Works on my machine"&lt;/em&gt; 🤷&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Requisitos de Deployment Moderno&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Reproducibilidad&lt;/strong&gt;: Mismo comportamiento en dev, staging, prod&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📦 Portabilidad&lt;/strong&gt;: Ejecuta en cualquier servidor Linux&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔒 Aislamiento&lt;/strong&gt;: Dependencias no interfieren con host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Velocidad&lt;/strong&gt;: Deploy en segundos, no horas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 Versionado&lt;/strong&gt;: Cada deploy es traceable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;↔️ Consistencia&lt;/strong&gt;: Python 3.13, UV, dependencias exactas&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Opciones para Deployment&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Método&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Contras&lt;/th&gt;
&lt;th&gt;Reproducibilidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manual (pip install)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;td&gt;Dependencias conflictivas&lt;/td&gt;
&lt;td&gt;❌ Ninguna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Virtual environments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Aislamiento Python&lt;/td&gt;
&lt;td&gt;No aísla sistema&lt;/td&gt;
&lt;td&gt;⚠️ Parcial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conda environments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-lenguaje&lt;/td&gt;
&lt;td&gt;Pesado, lento&lt;/td&gt;
&lt;td&gt;⚠️ Media&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VM images&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Aislamiento total&lt;/td&gt;
&lt;td&gt;Pesado (GBs), lento&lt;/td&gt;
&lt;td&gt;✅ Alta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker containers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ligero, rápido, portable&lt;/td&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;✅✅ &lt;strong&gt;Muy Alta&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nuestra elección&lt;/strong&gt;: &lt;strong&gt;Docker&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;La Magnitud del Problema&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Sin Containerización&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Setup en servidor nuevo:&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;# 1. Instalar Python 3.13 (20 minutos)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;python3.13
&lt;span class="c"&gt;# Error: "python3.13 not found in repository"&lt;/span&gt;
&lt;span class="c"&gt;# Compilar desde source... (60 minutos)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Instalar UV (5 minutos)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh

&lt;span class="c"&gt;# 3. Clonar repo y instalar deps (10 minutos)&lt;/span&gt;
git clone ...
&lt;span class="nb"&gt;cd &lt;/span&gt;project
uv &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;span class="c"&gt;# Error: "Conflicto con paquete del sistema"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Configurar env vars (5 minutos)&lt;/span&gt;
&lt;span class="c"&gt;# Copiar .env, ajustar paths...&lt;/span&gt;

&lt;span class="c"&gt;# 5. Debugging de problemas (120 minutos)&lt;/span&gt;
&lt;span class="c"&gt;# "¿Por qué no encuentra librerías?"&lt;/span&gt;
&lt;span class="c"&gt;# "¿OpenSSL version incompatible?"&lt;/span&gt;

TOTAL: 3-4 horas + frustración
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Con Docker:&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;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8000:8000 &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env username/lus-laboris-api:latest

TOTAL: 2 minutos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 &lt;strong&gt;La Solución: Docker Containers&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es Docker?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt; es una plataforma de containerización que permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;Empaquetar&lt;/strong&gt; aplicación + dependencias + runtime&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Ejecutar&lt;/strong&gt; de forma aislada en cualquier servidor&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Distribuir&lt;/strong&gt; via registries (Docker Hub, GCR)&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Versionar&lt;/strong&gt; cada build con tags&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Iniciar&lt;/strong&gt; en segundos (vs minutos de VMs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Container vs VM&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│         Virtual Machine Architecture        │
│                                             │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐      │
│  │  App 1  │ │  App 2  │ │  App 3  │      │
│  ├─────────┤ ├─────────┤ ├─────────┤      │
│  │ Python  │ │  Node   │ │  Java   │      │
│  ├─────────┤ ├─────────┤ ├─────────┤      │
│  │Guest OS │ │Guest OS │ │Guest OS │      │ Heavy!
│  │ (1GB+)  │ │ (1GB+)  │ │ (1GB+)  │      │
│  └─────────┘ └─────────┘ └─────────┘      │
│  ───────────────────────────────────       │
│           Hypervisor (VMware, KVM)         │
│  ───────────────────────────────────       │
│              Host OS (Linux)               │
│  ───────────────────────────────────       │
│                Hardware                    │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│         Docker Container Architecture       │
│                                             │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐      │
│  │  App 1  │ │  App 2  │ │  App 3  │      │
│  │ +Python │ │ +Node   │ │ +Java   │      │
│  │ +Libs   │ │ +Libs   │ │ +Libs   │      │ Light!
│  │ (100MB) │ │ (80MB)  │ │ (150MB) │      │
│  └─────────┘ └─────────┘ └─────────┘      │
│  ───────────────────────────────────       │
│          Docker Engine (containerd)        │
│  ───────────────────────────────────       │
│              Host OS (Linux)               │
│  ───────────────────────────────────       │
│                Hardware                    │
└─────────────────────────────────────────────┘

Ventajas de Containers:
✅ 10-100x más ligero
✅ Inicia en segundos (vs minutos)
✅ Menos overhead de CPU/RAM
✅ Comparte kernel del host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🏗️ &lt;strong&gt;Dockerfiles del Proyecto&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Dockerfile para Procesamiento (Batch)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;src/processing/Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Base image: Python 3.13 slim (Debian Bookworm)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.13.5-slim-bookworm&lt;/span&gt;

&lt;span class="c"&gt;# Copy UV from official image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/&lt;/span&gt;

&lt;span class="c"&gt;# Create non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash appuser

&lt;span class="c"&gt;# Set working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Environment variables&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/app/.venv/bin:$PATH"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;

&lt;span class="c"&gt;# Copy dependency files (leverage Docker cache)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser pyproject.toml uv.lock ./&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--locked&lt;/span&gt;

&lt;span class="c"&gt;# Copy source code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser extract_law_text.py ./&lt;/span&gt;

&lt;span class="c"&gt;# Switch to non-root user&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="c"&gt;# Entrypoint&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["uv", "run", "extract_law_text.py"]&lt;/span&gt;

&lt;span class="c"&gt;# CMD empty to allow flexible args&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Características clave:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Multi-stage&lt;/strong&gt;: Copia UV desde imagen oficial (no instalación)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Layer caching&lt;/strong&gt;: Dependencies primero, código después&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Non-root user&lt;/strong&gt;: Seguridad (usuario &lt;code&gt;appuser&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;UV locked&lt;/strong&gt;: Dependencias exactas con &lt;code&gt;uv.lock&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Flexible args&lt;/strong&gt;: CMD vacío permite pasar argumentos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Build:&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="nb"&gt;cd &lt;/span&gt;src/processing
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; username/lus-laboris-processing:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run:&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;# Con argumentos personalizados&lt;/span&gt;
docker run username/lus-laboris-processing:latest &lt;span class="nt"&gt;--input&lt;/span&gt; data.pdf &lt;span class="nt"&gt;--output&lt;/span&gt; output.json

&lt;span class="c"&gt;# Con volúmenes para datos&lt;/span&gt;
docker run &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/data:/app/data username/lus-laboris-processing:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Dockerfile para API (FastAPI)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;src/lus_laboris_api/Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Base image: Python 3.13 slim&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.13.5-slim-bookworm&lt;/span&gt;

&lt;span class="c"&gt;# Copy UV from official image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/&lt;/span&gt;

&lt;span class="c"&gt;# Install build dependencies and create user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        build-essential &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash apiuser

&lt;span class="c"&gt;# Set working directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Environment variables&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/app/.venv/bin:$PATH"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;

&lt;span class="c"&gt;# Copy dependency files (caching layer)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=apiuser:apiuser pyproject.toml uv.lock ./&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--locked&lt;/span&gt;

&lt;span class="c"&gt;# Copy source code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=apiuser:apiuser api ./api&lt;/span&gt;

&lt;span class="c"&gt;# Switch to non-root user&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; apiuser&lt;/span&gt;

&lt;span class="c"&gt;# Expose port&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="c"&gt;# Entrypoint&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["uv", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Diferencias con Dockerfile de procesamiento:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Build tools&lt;/strong&gt;: Incluye &lt;code&gt;build-essential&lt;/code&gt; para compilar deps&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Port exposure&lt;/strong&gt;: &lt;code&gt;EXPOSE 8000&lt;/code&gt; para Cloud Run&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Uvicorn entrypoint&lt;/strong&gt;: Inicia servidor ASGI&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cleanup&lt;/strong&gt;: Elimina apt cache para reducir tamaño&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Build:&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="nb"&gt;cd &lt;/span&gt;src/lus_laboris_api
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; username/lus-laboris-api:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run:&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;# Desarrollo local&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8000:8000 &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env username/lus-laboris-api:latest

&lt;span class="c"&gt;# Acceder a API&lt;/span&gt;
curl http://localhost:8000/api/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🐳 &lt;strong&gt;Docker Compose para la API&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;El archivo &lt;code&gt;src/lus_laboris_api/docker-compose.yml&lt;/code&gt; del proyecto es &lt;strong&gt;simple&lt;/strong&gt; y solo contiene la API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;legal-rag-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rj24/legal-rag-api:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;legal-rag-api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8000:8000&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;API_ENV_FILE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/.env&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../keys/public_key.pem:/app/api/keys/public_key.pem:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../.env:/app/.env:ro&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Características:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Solo API&lt;/strong&gt;: Qdrant y Phoenix se levantan con &lt;code&gt;services/manage_services.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Imagen pre-built&lt;/strong&gt;: Usa imagen de Docker Hub (no build local)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Monta secrets&lt;/strong&gt;: JWT key y .env desde host&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Restart policy&lt;/strong&gt;: Se reinicia si crash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Uso:&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;# PASO 1: Levantar servicios externos&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;services
./manage_services.sh  &lt;span class="c"&gt;# Opción 's' → 'Todos los servicios'&lt;/span&gt;

&lt;span class="c"&gt;# PASO 2: Levantar API&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../src/lus_laboris_api
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verificar servicios&lt;/span&gt;
curl http://localhost:6333/collections  &lt;span class="c"&gt;# Qdrant (del paso 1)&lt;/span&gt;
curl http://localhost:6006              &lt;span class="c"&gt;# Phoenix (del paso 1)&lt;/span&gt;
curl http://localhost:8000/api/health   &lt;span class="c"&gt;# API&lt;/span&gt;

&lt;span class="c"&gt;# Detener solo API&lt;/span&gt;
docker-compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Por qué separado?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Flexibilidad&lt;/strong&gt;: Puedes actualizar API sin tocar servicios&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Reutilización&lt;/strong&gt;: Qdrant/Phoenix sirven múltiples aplicaciones&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Desarrollo&lt;/strong&gt;: Puedes correr API local sin Docker, solo servicios en Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Opción Alternativa: Docker Compose Consolidado&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si prefieres levantar &lt;strong&gt;todo&lt;/strong&gt; con un solo comando, puedes crear &lt;code&gt;docker-compose.dev.yml&lt;/code&gt; en la raíz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.dev.yml (en raíz del proyecto)&lt;/span&gt;
&lt;span class="c1"&gt;# Archivo opcional para desarrollo rápido&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;qdrant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qdrant/qdrant:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6333:6333"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6334:6334"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;qdrant_storage:/qdrant/storage&lt;/span&gt;

  &lt;span class="na"&gt;phoenix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arizephoenix/phoenix:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6006:6006"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4317:4317"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090:9090"&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rj24/legal-rag-api:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./keys/public_key.pem:/app/api/keys/public_key.pem:ro&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;qdrant&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;phoenix&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;qdrant_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Uso:&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;# Levantar todo&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Detener todo&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📦 &lt;strong&gt;Optimización de Imágenes Docker&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Multi-Stage Builds (Opcional)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Aunque nuestro proyecto no usa multi-stage builds actualmente, es una técnica útil para reducir tamaño:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concepto:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stage 1 (Builder)&lt;/strong&gt;: Instala todas las build tools y dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage 2 (Runtime)&lt;/strong&gt;: Copia solo el &lt;code&gt;.venv&lt;/code&gt; compilado, sin build tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Beneficio potencial&lt;/strong&gt;: Imagen 40-50% más pequeña (elimina gcc, make, etc.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por qué no lo usamos:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;python:3.13-slim&lt;/code&gt; ya es bastante pequeño (~330MB total)&lt;/li&gt;
&lt;li&gt;UV hace el proceso muy rápido (no vale la pena la complejidad adicional)&lt;/li&gt;
&lt;li&gt;Multi-stage agrega tiempo de build&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. .dockerignore&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El archivo &lt;code&gt;.dockerignore&lt;/code&gt; previene copiar archivos innecesarios al build:&lt;/p&gt;

&lt;p&gt;Archivo &lt;code&gt;src/lus_laboris_api/.dockerignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Python
__pycache__/
*.pyc
*.pyo
*.pyd

# Virtual environments
.venv/
env/
venv/

# Data and output files
data/
*.db
*.json
*.csv

# Logs
*.log

# Git files
.git/
.gitignore

# Temporary Dockerfiles
Dockerfile.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Beneficios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Build más rápido&lt;/strong&gt;: No copia &lt;code&gt;.venv&lt;/code&gt;, &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt; (pueden ser GBs)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Imagen más pequeña&lt;/strong&gt;: Solo código fuente necesario&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Context más pequeño&lt;/strong&gt;: Docker build context ~10MB vs 500MB+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Archivos importantes que NO se ignoran:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api/&lt;/code&gt; (código fuente)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pyproject.toml&lt;/code&gt; (dependencies)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uv.lock&lt;/code&gt; (locked versions)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Layer Caching Strategy&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ MAL: Dependencies después de código&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; api ./api&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;span class="c"&gt;# Problema: Cambio en api/ → re-install de TODAS las deps&lt;/span&gt;

&lt;span class="c"&gt;# ✅ BIEN: Dependencies primero&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--locked&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; api ./api&lt;/span&gt;
&lt;span class="c"&gt;# Beneficio: Cambio en api/ → solo re-copy código (cache de deps)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mejora&lt;/strong&gt;: Build de 5 minutos → 30 segundos en iteraciones&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Build y Publicación en Docker Hub&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Script Automatizado&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;src/lus_laboris_api/docker_build_push.sh&lt;/code&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Load variables from .env&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"../../.env"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; allexport
    &lt;span class="nb"&gt;source&lt;/span&gt; ../../.env
    &lt;span class="nb"&gt;set&lt;/span&gt; +o allexport
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Validate required variables&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ ERROR: Variables requeridas no definidas"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Login to Docker Hub&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--password-stdin&lt;/span&gt;

&lt;span class="c"&gt;# Define tags&lt;/span&gt;
&lt;span class="nv"&gt;DATE_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;LATEST_TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;

&lt;span class="c"&gt;# Build image&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🏗️  Building image..."&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$DATE_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Tag as latest&lt;/span&gt;
docker tag &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$DATE_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
           &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$LATEST_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Push both tags&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"📤 Pushing to Docker Hub..."&lt;/span&gt;
docker push &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$DATE_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker push &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$LATEST_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Imágenes subidas a Docker Hub:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   &lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$DATE_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   &lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_HUB_USERNAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$DOCKER_IMAGE_NAME_RAG_API&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$LATEST_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Uso:&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="nb"&gt;cd &lt;/span&gt;src/lus_laboris_api
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x docker_build_push.sh
./docker_build_push.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&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;🏗️  Building image...
[+] Building 45.3s (12/12) FINISHED
 =&amp;gt; [1/6] FROM python:3.13.5-slim-bookworm
 =&amp;gt; [2/6] COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
 =&amp;gt; [3/6] RUN useradd --create-home apiuser
 =&amp;gt; [4/6] COPY pyproject.toml uv.lock ./
 =&amp;gt; [5/6] RUN uv sync --locked
 =&amp;gt; [6/6] COPY api ./api

📤 Pushing to Docker Hub...
The push refers to repository [docker.io/username/lus-laboris-api]
20241016: digest: sha256:abc123... size: 2415
latest: digest: sha256:abc123... size: 2415

✅ Imágenes subidas a Docker Hub:
   username/lus-laboris-api:20241016
   username/lus-laboris-api:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. GitHub Actions para Build Automático&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/docker-api-build-publish.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Publish Docker Image (API)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Manual trigger&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/lus_laboris_api/Dockerfile'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/lus_laboris_api/pyproject.toml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/lus_laboris_api/uv.lock'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/lus_laboris_api/api/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Hub&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_USERNAME }}&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_PASSWORD }}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get date tag&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date_tag&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "tag=$(date +'%Y%m%d')" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/lus_laboris_api&lt;/span&gt;
        &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-api:latest&lt;/span&gt;
          &lt;span class="s"&gt;${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-api:${{ steps.date_tag.outputs.tag }}&lt;/span&gt;
        &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-api:latest&lt;/span&gt;
        &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=inline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto-build&lt;/strong&gt;: Push a GitHub → build automático&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Buildx&lt;/strong&gt;: Multi-platform builds (amd64, arm64)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cache&lt;/strong&gt;: Reutiliza layers de builds anteriores&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Dual tags&lt;/strong&gt;: &lt;code&gt;latest&lt;/code&gt; + fecha para rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Casos de Uso Reales&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Desarrollo Local:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero desarrollo consistente sin instalar Python/UV"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Docker Compose levanta TODO&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;src/lus_laboris_api
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# API en http://localhost:8000&lt;/span&gt;
&lt;span class="c"&gt;# Qdrant en http://localhost:6333&lt;/span&gt;
&lt;span class="c"&gt;# Phoenix en http://localhost:6006&lt;/span&gt;

&lt;span class="c"&gt;# Código cambia → rebuild solo API:&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt; api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Onboarding de Nuevos Desarrolladores:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Nuevo dev necesita setup en 5 minutos"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Paso 1: Clonar repo&lt;/span&gt;
git clone https://github.com/user/lus-laboris-py.git
&lt;span class="nb"&gt;cd &lt;/span&gt;lus-laboris-py

&lt;span class="c"&gt;# Paso 2: Copiar .env&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Editar con tus API keys&lt;/span&gt;

&lt;span class="c"&gt;# Paso 3: Docker Compose&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;src/lus_laboris_api
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Paso 4: Verificar&lt;/span&gt;
curl http://localhost:8000/docs

&lt;span class="c"&gt;# ✅ DONE en 5 minutos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Deployment en Cloud Run:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Necesito deployar en GCP"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Build y push&lt;/span&gt;
./docker_build_push.sh

&lt;span class="c"&gt;# Deploy con gcloud&lt;/span&gt;
gcloud run deploy lus-laboris-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; username/lus-laboris-api:20241016 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; managed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1

&lt;span class="c"&gt;# ✅ API en producción en 3 minutos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Testing en CI/CD:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero correr tests en ambiente idéntico a producción"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/test.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build test image&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t api:test .&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests in container&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker run api:test pytest tests/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📊 &lt;strong&gt;Optimización de Imágenes&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Tamaños de Imagen&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Imagen&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Con deps&lt;/th&gt;
&lt;th&gt;Final&lt;/th&gt;
&lt;th&gt;Optimización&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API (sin optimizar)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;python:3.13 (1GB)&lt;/td&gt;
&lt;td&gt;+500MB&lt;/td&gt;
&lt;td&gt;1.5GB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API (slim)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;python:3.13-slim (150MB)&lt;/td&gt;
&lt;td&gt;+180MB&lt;/td&gt;
&lt;td&gt;330MB&lt;/td&gt;
&lt;td&gt;✅ 78% reducción&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API (alpine)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;python:3.13-alpine (50MB)&lt;/td&gt;
&lt;td&gt;+200MB&lt;/td&gt;
&lt;td&gt;250MB&lt;/td&gt;
&lt;td&gt;✅✅ 83% reducción&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;python:3.13-slim&lt;/td&gt;
&lt;td&gt;+120MB&lt;/td&gt;
&lt;td&gt;270MB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nuestra elección&lt;/strong&gt;: &lt;code&gt;python:3.13-slim&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Balance tamaño/compatibilidad&lt;/li&gt;
&lt;li&gt;✅ Debian-based (mayor compatibilidad de deps)&lt;/li&gt;
&lt;li&gt;✅ 78% más pequeño que &lt;code&gt;python:3.13&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Técnicas de Optimización&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Cleanup de apt cache:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; build-essential &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="c"&gt;# ⬅️ Elimina 50-100MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. UV locked dependencies:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml uv.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--locked&lt;/span&gt;  &lt;span class="c"&gt;# ⬅️ No descarga extras, exactas versiones&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Non-root user:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd apiuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; apiuser  # ⬅️ Seguridad + compatibilidad Cloud Run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Minimize layers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ MAL: 3 layers&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; build-essential
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# ✅ BIEN: 1 layer&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; build-essential &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;El Impacto Transformador&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Antes de Docker:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Setup time&lt;/strong&gt;: 2-4 horas por servidor&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;"Works on my machine"&lt;/strong&gt;: Constante debugging de entorno&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Dependency hell&lt;/strong&gt;: Conflictos con sistema&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Inconsistencia&lt;/strong&gt;: Dev ≠ Staging ≠ Prod&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Desperdicio&lt;/strong&gt;: Múltiples VMs para aislamiento&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Después de Docker:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Setup time&lt;/strong&gt;: 2-5 minutos con Docker Compose&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Guaranteed consistency&lt;/strong&gt;: Dev = Staging = Prod&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Dependency isolation&lt;/strong&gt;: Zero conflictos&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Perfect parity&lt;/strong&gt;: Mismo container en todos lados&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Efficiency&lt;/strong&gt;: Múltiples containers en 1 servidor&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas de Mejora:&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Sin Docker&lt;/th&gt;
&lt;th&gt;Con Docker&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-4 horas&lt;/td&gt;
&lt;td&gt;2-5 minutos&lt;/td&gt;
&lt;td&gt;-95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment issues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frecuentes&lt;/td&gt;
&lt;td&gt;Raros&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deploy time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-60 min&lt;/td&gt;
&lt;td&gt;2-3 min&lt;/td&gt;
&lt;td&gt;-95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;30 seg&lt;/td&gt;
&lt;td&gt;-97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Onboarding (new dev)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-2 días&lt;/td&gt;
&lt;td&gt;10 minutos&lt;/td&gt;
&lt;td&gt;-99%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;Lecciones Aprendidas&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Slim &amp;gt; Full &amp;gt; Alpine&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Para Python, &lt;code&gt;python:3.13-slim&lt;/code&gt; es el sweet spot. Alpine tiene problemas con deps compiladas.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Copy Dependencies First&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Layer caching es crítico. Dependencies cambian poco, código cambia mucho.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Non-Root User es Requerido&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Cloud Run y muchos orchestrators requieren non-root. Hazlo desde el principio.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. UV en Docker es Magia&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;UV es 10-100x más rápido que pip. Copia desde imagen oficial = zero install time.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. .dockerignore es Crítico&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sin .dockerignore, builds lentos y imágenes grandes (copia &lt;code&gt;.venv&lt;/code&gt;, &lt;code&gt;data/&lt;/code&gt;, etc.)&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. Non-Root User es Requerido&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Cloud Run y otros orchestrators requieren containers que NO corran como root. Ambos Dockerfiles usan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash apiuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; apiuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Beneficios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Seguridad&lt;/strong&gt;: Limita permisos del proceso&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Compatibilidad&lt;/strong&gt;: Cloud Run, Kubernetes requieren non-root&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Best practice&lt;/strong&gt;: Principio de mínimo privilegio&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Propósito Más Grande&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Docker no es solo containerización - es &lt;strong&gt;portabilidad universal&lt;/strong&gt;. Al empaquetar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📦 Runtime&lt;/strong&gt;: Python 3.13&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📚 Dependencies&lt;/strong&gt;: UV + todas las libs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚙️ Configuration&lt;/strong&gt;: Environment variables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔧 Code&lt;/strong&gt;: Aplicación completa&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En un &lt;strong&gt;artifact inmutable&lt;/strong&gt;, logramos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Build once, run anywhere&lt;/strong&gt;: Dev, staging, prod, cloud, on-prem&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Versionado exacto&lt;/strong&gt;: Cada imagen es traceable (tag + digest)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Rollback instant&lt;/strong&gt;: Cambiar tag en deploy = rollback&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Scaling horizontal&lt;/strong&gt;: Múltiples containers de la misma imagen&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Testing aislado&lt;/strong&gt;: Tests en container = env idéntico a prod&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero config drift&lt;/strong&gt;: Configuración versionada en Dockerfile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estamos eliminando la categoría completa de errores "funciona en mi máquina" y acelerando deployment de horas a minutos.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Recursos y Enlaces&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Repositorio del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Documentación Técnica&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Dockerfile&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/Dockerfile" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/Dockerfile&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing Dockerfile&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/processing/Dockerfile" rel="noopener noreferrer"&gt;&lt;code&gt;src/processing/Dockerfile&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/docker-compose.yml&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Script&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/docker_build_push.sh" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/docker_build_push.sh&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Recursos Externos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Docs&lt;/strong&gt;: &lt;a href="https://docs.docker.com/" rel="noopener noreferrer"&gt;docs.docker.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub&lt;/strong&gt;: &lt;a href="https://hub.docker.com/" rel="noopener noreferrer"&gt;hub.docker.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile Best Practices&lt;/strong&gt;: &lt;a href="https://docs.docker.com/develop/dev-best-practices/" rel="noopener noreferrer"&gt;docs.docker.com/develop/dev-best-practices&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose Docs&lt;/strong&gt;: &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;docs.docker.com/compose&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Próximo Post&lt;/strong&gt;: LLPY-13 - CI/CD con GitHub Actions&lt;/p&gt;

&lt;p&gt;En el siguiente post exploraremos el sistema completo de CI/CD con 7 workflows automatizados: quality checks, Docker builds, Terraform apply, y deployments a GCP.&lt;/p&gt;

</description>
      <category>containers</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>LLPY-11: Terraform - Infraestructura como Código</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Fri, 17 Oct 2025 18:27:29 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llpy-11-terraform-infraestructura-como-codigo-57dp</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llpy-11-terraform-infraestructura-como-codigo-57dp</guid>
      <description>&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Desafío de Gestionar Infraestructura Cloud&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagina que necesitas desplegar tu sistema RAG en GCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;VM para Qdrant&lt;/strong&gt; (Compute Engine)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;API en Cloud Run&lt;/strong&gt; (FastAPI)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Batch Job&lt;/strong&gt; (Cloud Run Job para procesamiento)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Storage&lt;/strong&gt; (Google Cloud Storage)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Secrets&lt;/strong&gt; (Secret Manager para .env y JWT keys)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;El problema&lt;/strong&gt;: ¿Cómo creas, actualizas y gestionas toda esta infraestructura de forma reproducible, versionada y colaborativa?&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Opciones para Gestionar Infraestructura&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Método&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Contras&lt;/th&gt;
&lt;th&gt;Reproducibilidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Console UI (manual)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fácil, visual&lt;/td&gt;
&lt;td&gt;Propenso a errores, no versionado&lt;/td&gt;
&lt;td&gt;❌ Ninguna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gcloud CLI scripts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatizado&lt;/td&gt;
&lt;td&gt;Scripts frágiles, difícil rollback&lt;/td&gt;
&lt;td&gt;⚠️ Limitada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Formation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IaC nativo AWS&lt;/td&gt;
&lt;td&gt;Solo AWS&lt;/td&gt;
&lt;td&gt;✅ Alta (AWS only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pulumi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple lenguajes&lt;/td&gt;
&lt;td&gt;Requiere runtime&lt;/td&gt;
&lt;td&gt;✅ Alta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terraform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Declarativo, multi-cloud&lt;/td&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;✅✅ &lt;strong&gt;Muy Alta&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nuestra elección&lt;/strong&gt;: &lt;strong&gt;Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;La Magnitud del Problema&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Requisitos de Infraestructura&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Para el proyecto Lus Laboris necesitamos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;🗄️ Google Cloud Storage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bucket para datos procesados&lt;/li&gt;
&lt;li&gt;Bucket para Terraform state (remoto)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;🖥️ Compute Engine VM&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VM para Qdrant (vector database)&lt;/li&gt;
&lt;li&gt;Ubuntu 22.04 LTS&lt;/li&gt;
&lt;li&gt;Firewall rules (ports 6333, 6334, 22)&lt;/li&gt;
&lt;li&gt;SPOT instance (cost optimization)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;🚀 Cloud Run Service&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI container&lt;/li&gt;
&lt;li&gt;Secrets montados de Secret Manager&lt;/li&gt;
&lt;li&gt;Auto-scaling (0-10 instances)&lt;/li&gt;
&lt;li&gt;2 CPU, 2Gi RAM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;⏰ Cloud Run Job&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch processing (scheduled)&lt;/li&gt;
&lt;li&gt;Cron schedule&lt;/li&gt;
&lt;li&gt;Logs y notificaciones&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;🔐 Secret Manager&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; file (all API config)&lt;/li&gt;
&lt;li&gt;JWT public key&lt;/li&gt;
&lt;li&gt;IAM permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Desafíos Sin IaC&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sin Terraform:&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;Developer 1 crea VM via Console → Settings no documentados
Developer 2 necesita replicar → "¿Qué settings usaste?"
Developer 1: "Hmm... creo que era e2-medium con 20GB..."
Developer 2: "¿Y el firewall?"
Developer 1: "Olvidé... tendrás que experimentar"

Disaster Recovery: "La VM se borró, ¿cómo la recreo?"
Team: 🤷 "No hay backup de la configuración"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Con Terraform:&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;git clone repo
terraform apply
✅ Toda la infraestructura recreada en 5 minutos
✅ Configuración exacta versionada en Git
✅ Documentación viva en código
✅ Rollback a cualquier versión anterior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 &lt;strong&gt;La Solución: Terraform Infrastructure as Code&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es Terraform?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Terraform&lt;/strong&gt; (por HashiCorp) es una herramienta de Infrastructure as Code (IaC) que permite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📝 &lt;strong&gt;Definir&lt;/strong&gt; infraestructura en archivos declarativos (HCL)&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Planear&lt;/strong&gt; cambios antes de aplicarlos&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Aplicar&lt;/strong&gt; cambios de forma idempotente&lt;/li&gt;
&lt;li&gt;🗑️ &lt;strong&gt;Destruir&lt;/strong&gt; infraestructura limpiamente&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Versionado&lt;/strong&gt; en Git como código&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Principios de Terraform&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Declarativo&lt;/strong&gt;: Describes el estado deseado, no los pasos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotente&lt;/strong&gt;: Ejecutar N veces = mismo resultado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan-Apply&lt;/strong&gt;: Preview de cambios antes de ejecutar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Management&lt;/strong&gt;: Trackea estado actual vs deseado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular&lt;/strong&gt;: Componentes reutilizables&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🏗️ &lt;strong&gt;Arquitectura de Terraform en el Proyecto&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Estructura de Carpetas&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── main.tf                    # Orquestación de módulos
├── variables.tf               # Definición de variables
├── terraform.tfvars           # Valores de variables
├── providers.tf               # Configuración de GCP provider
├── tf_menu.sh                 # Script interactivo para ops comunes
├── README.md                  # Documentación
│
└── modules/                   # Módulos reutilizables
    ├── gcs/                   # Google Cloud Storage
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    │
    ├── compute_engine/        # VM para Qdrant
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    │
    ├── cloud_run_service/     # FastAPI en Cloud Run
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    │
    ├── cloud_run_job/         # Batch processing
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    │
    └── secret_manager/        # Secrets management
        ├── main.tf
        ├── variables.tf
        └── outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Principio de diseño&lt;/strong&gt;: Un módulo = un recurso lógico reutilizable&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Implementación Paso a Paso&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Configuración de Providers&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;terraform/providers.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform Configuration&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/google"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 7.1.1"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"gcs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"py-labor-law-rag-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform/state"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Provider de Google Cloud Platform&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"google"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&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;Características:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Remote state&lt;/strong&gt;: State almacenado en GCS (no local)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Versión fijada&lt;/strong&gt;: &lt;code&gt;~&amp;gt; 7.1.1&lt;/code&gt; = 7.1.x (semver)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;State locking&lt;/strong&gt;: GCS provee locking automático&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Team collaboration&lt;/strong&gt;: Múltiples devs comparten state&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Definición de Variables&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;terraform/variables.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Basic GCP Configuration&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GCP Project ID"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GCP Region"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project_number"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GCP Project Number (for service accounts)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Google Cloud Storage&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Name for the GCS bucket"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Compute Engine (Qdrant VM)&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"qdrant_vm_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Name for the Qdrant VM"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"qdrant-vm"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"qdrant_vm_machine_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Machine type for Qdrant VM"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"e2-medium"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"qdrant_vm_zone"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Zone for Qdrant VM"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-central1-a"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"qdrant_vm_disk_size"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Boot disk size in GB"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Cloud Run Service (API)&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_service_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Name of the Cloud Run service"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lus-laboris-api"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_image"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Docker image for the API"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_container_port"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Container port for the API"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_cpu"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Number of CPUs for the API"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_memory"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Memory for the API (e.g., 2Gi)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2Gi"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_min_instance_count"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Minimum number of instances"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;# Scale to zero for cost savings&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_max_instance_count"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Maximum number of instances"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_timeout"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Request timeout"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"300s"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Secret Manager&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"api_env_secret_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Secret ID for .env file"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"jwt_public_key_secret_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Secret ID for JWT public key"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Cloud Run Job (Batch)&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"job_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Name of the Cloud Run Job"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"image"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Docker image for the job"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"args"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Arguments for the job"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;default&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;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"schedule"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Cron schedule for the job"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0 2 * * *"&lt;/span&gt;  &lt;span class="c1"&gt;# 2 AM daily&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"notify_email"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Email for job notifications"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3. Módulo: Compute Engine (VM para Qdrant)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El módulo &lt;code&gt;compute_engine&lt;/code&gt; crea la VM para Qdrant con configuración optimizada:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué crea?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VM Instance&lt;/strong&gt; (&lt;code&gt;google_compute_instance.qdrant_vm&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OS: Ubuntu 22.04 LTS&lt;/li&gt;
&lt;li&gt;Machine type: &lt;code&gt;e2-medium&lt;/code&gt; (2 vCPU, 4GB RAM)&lt;/li&gt;
&lt;li&gt;Disco: 20GB (configurable)&lt;/li&gt;
&lt;li&gt;Provisioning: &lt;strong&gt;SPOT&lt;/strong&gt; (preemptible) ← 80% más barato&lt;/li&gt;
&lt;li&gt;Network: Default con IP pública efímera&lt;/li&gt;
&lt;li&gt;Tags: &lt;code&gt;qdrant-server&lt;/code&gt;, &lt;code&gt;http-server&lt;/code&gt;, &lt;code&gt;https-server&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Firewall Rule&lt;/strong&gt; (&lt;code&gt;google_compute_firewall.qdrant_firewall&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Permite TCP en puertos: &lt;code&gt;6333&lt;/code&gt; (HTTP), &lt;code&gt;6334&lt;/code&gt; (gRPC), &lt;code&gt;22&lt;/code&gt; (SSH)&lt;/li&gt;
&lt;li&gt;Source: &lt;code&gt;0.0.0.0/0&lt;/code&gt; (⚠️ en producción, restringir)&lt;/li&gt;
&lt;li&gt;Target: VMs con tag &lt;code&gt;qdrant-server&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Outputs&lt;/strong&gt; del módulo&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vm_external_ip&lt;/code&gt;: Para conectar desde API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vm_internal_ip&lt;/code&gt;: Para comunicación interna en GCP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vm_name&lt;/code&gt; y &lt;code&gt;vm_zone&lt;/code&gt;: Para referencia&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Ventajas de SPOT instances:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💰 &lt;strong&gt;80% más barato&lt;/strong&gt; que VM regular&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Preemptible&lt;/strong&gt;: Puede interrumpirse (OK para Qdrant con persistencia)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Ideal para workloads stateful&lt;/strong&gt; con almacenamiento persistente&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Módulo: Cloud Run Service (FastAPI)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El módulo &lt;code&gt;cloud_run_service&lt;/code&gt; despliega la API en Cloud Run:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué crea?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloud Run Service&lt;/strong&gt; (&lt;code&gt;google_cloud_run_v2_service.api_service&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container: Docker image desde Docker Hub o GCR&lt;/li&gt;
&lt;li&gt;Puerto: 8000 (FastAPI)&lt;/li&gt;
&lt;li&gt;CPU: 2 cores&lt;/li&gt;
&lt;li&gt;Memoria: 2Gi&lt;/li&gt;
&lt;li&gt;Scaling: 0-10 instances (scale-to-zero habilitado)&lt;/li&gt;
&lt;li&gt;Timeout: 300s (5 minutos)&lt;/li&gt;
&lt;li&gt;Deletion protection: false (permite destroy fácil)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IAM Policy&lt;/strong&gt; (acceso público)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role: &lt;code&gt;roles/run.invoker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Members: &lt;code&gt;allUsers&lt;/code&gt; (cualquiera puede llamar la API)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Traffic Routing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100% del tráfico a latest revision&lt;/li&gt;
&lt;li&gt;Deployment strategy: Rolling update&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Características clave:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Scale to zero&lt;/strong&gt;: Min instances = 0 → ahorro de costos cuando no hay tráfico&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto-scaling&lt;/strong&gt;: Escala automáticamente según requests&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Serverless&lt;/strong&gt;: No gestión de servers&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Pay-per-use&lt;/strong&gt;: Solo pagas cuando hay requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nota sobre Secrets:&lt;/strong&gt;&lt;br&gt;
Los secrets (.env y JWT key) se montan vía GitHub Actions workflow, no directamente en Terraform. El módulo básico crea el servicio, y el workflow &lt;code&gt;update-api-secrets-deploy.yml&lt;/code&gt; actualiza el servicio con los secrets.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;5. Módulo: Secret Manager&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El módulo &lt;code&gt;secret_manager&lt;/code&gt; crea secrets en GCP Secret Manager:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué crea?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Secret para .env&lt;/strong&gt; (&lt;code&gt;google_secret_manager_secret.api_env_file&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ID: Configurable (típicamente &lt;code&gt;api-env-file&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Replication: Automática (multi-region)&lt;/li&gt;
&lt;li&gt;Contenido: Se agrega después vía GitHub Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Secret para JWT public key&lt;/strong&gt; (&lt;code&gt;google_secret_manager_secret.jwt_public_key&lt;/code&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ID: Configurable (típicamente &lt;code&gt;jwt-public-key&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Replication: Automática&lt;/li&gt;
&lt;li&gt;Contenido: Public key agregada vía GitHub Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IAM Permissions&lt;/strong&gt; (para Cloud Run)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role: &lt;code&gt;roles/secretmanager.secretAccessor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Member: Service Account de Cloud Run&lt;/li&gt;
&lt;li&gt;Permite a Cloud Run leer los secrets&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Flujo de trabajo:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; crea los secret placeholders (vacíos)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; agrega el contenido real de los secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run&lt;/strong&gt; monta los secrets como archivos&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Separación&lt;/strong&gt;: Terraform gestiona estructura, GitHub Actions gestiona contenido sensible&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Seguridad&lt;/strong&gt;: Secrets no versionados en código&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Rotación&lt;/strong&gt;: Fácil actualizar secrets sin cambiar infraestructura&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;6. Main File (Orquestación)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Archivo &lt;code&gt;terraform/main.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Google Cloud Storage&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"gcs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/gcs"&lt;/span&gt;
  &lt;span class="nx"&gt;project_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Compute Engine VM (Qdrant)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"compute_engine"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/compute_engine"&lt;/span&gt;

  &lt;span class="nx"&gt;vm_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qdrant_vm_name&lt;/span&gt;
  &lt;span class="nx"&gt;machine_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qdrant_vm_machine_type&lt;/span&gt;
  &lt;span class="nx"&gt;zone&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qdrant_vm_zone&lt;/span&gt;
  &lt;span class="nx"&gt;disk_size&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qdrant_vm_disk_size&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Secret Manager (MUST be created BEFORE Cloud Run)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"secret_manager"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/secret_manager"&lt;/span&gt;

  &lt;span class="nx"&gt;project_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;cloud_run_service_account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_number&lt;/span&gt;
  &lt;span class="nx"&gt;api_env_secret_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_env_secret_id&lt;/span&gt;
  &lt;span class="nx"&gt;jwt_public_key_secret_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwt_public_key_secret_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Cloud Run Service (API)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"cloud_run_service"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/cloud_run_service"&lt;/span&gt;

  &lt;span class="nx"&gt;project_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_service_name&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_image&lt;/span&gt;
  &lt;span class="nx"&gt;container_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_container_port&lt;/span&gt;
  &lt;span class="nx"&gt;cpu&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_cpu&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_memory&lt;/span&gt;
  &lt;span class="nx"&gt;min_instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_min_instance_count&lt;/span&gt;
  &lt;span class="nx"&gt;max_instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_max_instance_count&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_timeout&lt;/span&gt;
  &lt;span class="nx"&gt;env_secret_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env_secret_name&lt;/span&gt;
  &lt;span class="nx"&gt;jwt_secret_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwt_secret_name&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_manager&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Cloud Run Job (Batch Processing)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"cloud_run_job"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/cloud_run_job"&lt;/span&gt;
  &lt;span class="nx"&gt;project_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;job_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job_name&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;
  &lt;span class="nx"&gt;args&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;
  &lt;span class="nx"&gt;schedule&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;
  &lt;span class="nx"&gt;notify_email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notify_email&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Outputs&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"qdrant_vm_external_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"External IP for Qdrant VM"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compute_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vm_external_ip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"api_service_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"URL of the deployed API"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloud_run_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Name of the GCS bucket"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔄 &lt;strong&gt;Workflow de Uso&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Inicialización&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Navegar a directorio terraform&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;terraform

&lt;span class="c"&gt;# Configurar credenciales&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"../.gcpcredentials/service-account.json"&lt;/span&gt;

&lt;span class="c"&gt;# Inicializar Terraform (descargar providers, configurar backend)&lt;/span&gt;
terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&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;Initializing the backend...
Initializing modules...
Initializing provider plugins...
- Finding hashicorp/google versions matching "~&amp;gt; 6.0"...
- Installing hashicorp/google v6.8.0...

Terraform has been successfully initialized!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Plan (Preview de Cambios)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ver qué cambios se aplicarán&lt;/span&gt;
terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&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;Terraform will perform the following actions:

  # module.compute_engine.google_compute_firewall.qdrant_firewall will be created
  + resource "google_compute_firewall" "qdrant_firewall" {
      + name    = "qdrant-vm-firewall"
      + network = "default"
      + ports   = ["6333", "6334", "22"]
    }

  # module.compute_engine.google_compute_instance.qdrant_vm will be created
  + resource "google_compute_instance" "qdrant_vm" {
      + name         = "qdrant-vm"
      + machine_type = "e2-medium"
      + zone         = "us-central1-a"
    }

  # module.cloud_run_service.google_cloud_run_v2_service.api_service will be created
  + resource "google_cloud_run_v2_service" "api_service" {
      + name     = "lus-laboris-api"
      + location = "us-central1"
    }

Plan: 15 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3. Apply (Ejecutar Cambios)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Aplicar cambios&lt;/span&gt;
terraform apply

&lt;span class="c"&gt;# O sin confirmación (CI/CD):&lt;/span&gt;
terraform apply &lt;span class="nt"&gt;-auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&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;module.secret_manager.google_secret_manager_secret.api_env_file: Creating...
module.secret_manager.google_secret_manager_secret.jwt_public_key: Creating...
module.compute_engine.google_compute_firewall.qdrant_firewall: Creating...
module.gcs.google_storage_bucket.bucket: Creating...
module.compute_engine.google_compute_instance.qdrant_vm: Creating...

...

Apply complete! Resources: 15 added, 0 changed, 0 destroyed.

Outputs:

api_service_url = "https://lus-laboris-api-abc123-uc.a.run.app"
bucket_name = "my-data-bucket"
qdrant_vm_external_ip = "34.123.45.67"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;4. Verificación&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ver outputs&lt;/span&gt;
terraform output

&lt;span class="c"&gt;# Output específico&lt;/span&gt;
terraform output api_service_url

&lt;span class="c"&gt;# Health check de la API&lt;/span&gt;
curl &lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; api_service_url&lt;span class="si"&gt;)&lt;/span&gt;/api/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;5. Destroy (Eliminar Infraestructura)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Eliminar TODA la infraestructura&lt;/span&gt;
terraform destroy

&lt;span class="c"&gt;# Con auto-approve (cuidado!)&lt;/span&gt;
terraform destroy &lt;span class="nt"&gt;-auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Script Interactivo: tf_menu.sh&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;El proyecto incluye un script bash &lt;strong&gt;sofisticado&lt;/strong&gt; que automatiza operaciones Terraform:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hace &lt;code&gt;tf_menu.sh&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Setea credenciales GCP automáticamente&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Busca archivo JSON en &lt;code&gt;.gcpcredentials/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Exporta &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Genera &lt;code&gt;terraform.tfvars&lt;/code&gt; desde &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lee variables de &lt;code&gt;.env&lt;/code&gt; del proyecto&lt;/li&gt;
&lt;li&gt;Crea &lt;code&gt;terraform.tfvars&lt;/code&gt; automáticamente&lt;/li&gt;
&lt;li&gt;Valida formato de variables (VM name, zone, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Valida ambiente antes de ejecutar&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verifica que credenciales estén seteadas&lt;/li&gt;
&lt;li&gt;Verifica que &lt;code&gt;terraform.tfvars&lt;/code&gt; exista&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ofrece menú interactivo&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ========= Terraform Menu =========
   1) Setear GOOGLE_APPLICATION_CREDENTIALS
   2) Crear archivo terraform.tfvars
   3) terraform init
   4) terraform plan
   5) terraform apply
   6) terraform destroy
   7) Salir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Uso del script:&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="nb"&gt;cd &lt;/span&gt;terraform
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x tf_menu.sh

&lt;span class="c"&gt;# Modo interactivo&lt;/span&gt;
./tf_menu.sh

&lt;span class="c"&gt;# Modo no-interactivo (para scripting)&lt;/span&gt;
./tf_menu.sh 1   &lt;span class="c"&gt;# Setear credenciales&lt;/span&gt;
./tf_menu.sh 2   &lt;span class="c"&gt;# Crear tfvars&lt;/span&gt;
./tf_menu.sh 3   &lt;span class="c"&gt;# Init&lt;/span&gt;
./tf_menu.sh 4   &lt;span class="c"&gt;# Plan&lt;/span&gt;
./tf_menu.sh 5   &lt;span class="c"&gt;# Apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas del script:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Automatiza setup&lt;/strong&gt;: Crea tfvars desde .env&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Validaciones&lt;/strong&gt;: Previene errores comunes&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;DRY&lt;/strong&gt;: No duplicar variables entre .env y tfvars&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Developer-friendly&lt;/strong&gt;: Menú en español, mensajes claros&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔄 &lt;strong&gt;Integración con CI/CD (GitHub Actions)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Archivo &lt;code&gt;.github/workflows/terraform-apply-on-tf-change.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply on TF Change&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terraform/**/*.tf'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terraform/**/*.tfvars'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GSA_KEY }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Terraform&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;terraform_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.9.0&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Validate&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform validate&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -out=tfplan&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply -auto-approve tfplan&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Show Outputs&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto-apply&lt;/strong&gt;: Cambios en &lt;code&gt;.tf&lt;/code&gt; files → auto-deploy&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Validation&lt;/strong&gt;: Validate antes de apply&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Plan saved&lt;/strong&gt;: Plan guardado para apply exacto&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Outputs visible&lt;/strong&gt;: Ver resultados en logs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Casos de Uso Reales&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Replicar Ambiente:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Necesito crear un ambiente de staging idéntico a producción"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Crear workspace para staging&lt;/span&gt;
terraform workspace new staging

&lt;span class="c"&gt;# Aplicar con variables de staging&lt;/span&gt;
terraform apply &lt;span class="nt"&gt;-var-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"staging.tfvars"&lt;/span&gt;

&lt;span class="c"&gt;# Resultado: Infraestructura idéntica en minutos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Disaster Recovery:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"La VM de Qdrant se eliminó accidentalmente"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Ver estado actual&lt;/span&gt;
terraform state list

&lt;span class="c"&gt;# Recrear solo la VM&lt;/span&gt;
terraform apply &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module.compute_engine

&lt;span class="c"&gt;# Resultado: VM recreada con configuración exacta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Actualizar Recursos:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Necesito aumentar RAM de la API de 2Gi a 4Gi"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;// terraform/terraform.tfvars&lt;/span&gt;
&lt;span class="nx"&gt;api_memory&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"4Gi"&lt;/span&gt;  &lt;span class="c1"&gt;// Era "2Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply

&lt;span class="c"&gt;# Resultado: Cloud Run actualizado con zero downtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Múltiples Ambientes:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero dev, staging, y prod con diferentes specs"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Crear workspaces&lt;/span&gt;
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

&lt;span class="c"&gt;# Usar variables por workspace&lt;/span&gt;
terraform workspace &lt;span class="k"&gt;select &lt;/span&gt;dev
terraform apply &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api_cpu=1"&lt;/span&gt; &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api_memory=1Gi"&lt;/span&gt;

terraform workspace &lt;span class="k"&gt;select &lt;/span&gt;prod
terraform apply &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api_cpu=4"&lt;/span&gt; &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api_memory=8Gi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;El Impacto Transformador&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Antes de Terraform:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🖱️ &lt;strong&gt;Manual deployment&lt;/strong&gt;: Click en console por 30-60 minutos&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Documentación&lt;/strong&gt;: Notas dispersas, desactualizadas&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Errores&lt;/strong&gt;: "Olvidé abrir el puerto 6334"&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Replicación&lt;/strong&gt;: Imposible crear ambiente idéntico&lt;/li&gt;
&lt;li&gt;💥 &lt;strong&gt;Disaster recovery&lt;/strong&gt;: Días para recrear infraestructura&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Después de Terraform:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Auto deployment&lt;/strong&gt;: &lt;code&gt;terraform apply&lt;/code&gt; = 5-10 minutos&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;Documentación viva&lt;/strong&gt;: El código ES la documentación&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Sin errores&lt;/strong&gt;: Configuración probada y versionada&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Replicación perfecta&lt;/strong&gt;: &lt;code&gt;terraform apply&lt;/code&gt; en cualquier ambiente&lt;/li&gt;
&lt;li&gt;💥 &lt;strong&gt;Disaster recovery&lt;/strong&gt;: Minutos para recrear todo&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas de Mejora:&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Sin IaC&lt;/th&gt;
&lt;th&gt;Con Terraform&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tiempo de setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-60 min&lt;/td&gt;
&lt;td&gt;5-10 min&lt;/td&gt;
&lt;td&gt;-80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Errores de config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frecuentes&lt;/td&gt;
&lt;td&gt;Raros&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reproducibilidad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Imposible&lt;/td&gt;
&lt;td&gt;Perfecta&lt;/td&gt;
&lt;td&gt;+100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Versionado&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Sí (Git)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git revert&lt;/code&gt; + apply&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team collaboration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Caos&lt;/td&gt;
&lt;td&gt;Sincronizado&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;Lecciones Aprendidas&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Remote State es Obligatorio&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Nunca uses state local para proyectos reales. GCS backend permite colaboración y previene conflictos.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Módulos = Reutilización&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Un módulo bien diseñado se puede usar en múltiples proyectos con mínimos ajustes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Variables con Defaults Sensatos&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Defaults buenos = menos config required = menos errores.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Outputs son Documentación&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Outputs muestran información crítica (IPs, URLs) que necesitas después del deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. depends_on para Orden&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Algunos recursos deben crearse en orden (Secret Manager → Cloud Run). Usa &lt;code&gt;depends_on&lt;/code&gt; explícitamente.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. SPOT Instances = 80% Ahorro&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Para workloads tolerantes a interrupciones (como Qdrant con persistencia), SPOT instances son oro.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Propósito Más Grande&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Terraform no es solo "automatización" - es &lt;strong&gt;infraestructura como producto&lt;/strong&gt;. Al tratar infraestructura como código:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📝 Versionado&lt;/strong&gt;: Cada cambio en Git con diff, blame, history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;👥 Colaboración&lt;/strong&gt;: Pull requests para cambios de infraestructura&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔍 Review&lt;/strong&gt;: Code review de infraestructura antes de aplicar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Rollback&lt;/strong&gt;: &lt;code&gt;git revert&lt;/code&gt; + &lt;code&gt;terraform apply&lt;/code&gt; = rollback instant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📚 Documentación&lt;/strong&gt;: El código no miente, siempre está actualizado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧪 Testing&lt;/strong&gt;: Ambientes efímeros para testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🚀 Velocity&lt;/strong&gt;: De idea a producción en minutos, no horas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estamos construyendo infraestructura con la misma calidad, rigor y velocidad que el código de aplicación.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Recursos y Enlaces&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Repositorio del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Documentación Técnica&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Main&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/terraform/main.tf" rel="noopener noreferrer"&gt;&lt;code&gt;terraform/main.tf&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute Engine Module&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/tree/main/terraform/modules/compute_engine" rel="noopener noreferrer"&gt;&lt;code&gt;terraform/modules/compute_engine/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run Module&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/tree/main/terraform/modules/cloud_run_service" rel="noopener noreferrer"&gt;&lt;code&gt;terraform/modules/cloud_run_service/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform README&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/terraform/README.md" rel="noopener noreferrer"&gt;&lt;code&gt;terraform/README.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Recursos Externos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Docs&lt;/strong&gt;: &lt;a href="https://www.terraform.io/docs" rel="noopener noreferrer"&gt;terraform.io/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP Provider&lt;/strong&gt;: &lt;a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs" rel="noopener noreferrer"&gt;registry.terraform.io/providers/hashicorp/google&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Best Practices&lt;/strong&gt;: &lt;a href="https://www.terraform-best-practices.com/" rel="noopener noreferrer"&gt;terraform-best-practices.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HashiCorp Learn&lt;/strong&gt;: &lt;a href="https://learn.hashicorp.com/terraform" rel="noopener noreferrer"&gt;learn.hashicorp.com/terraform&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Próximo Post&lt;/strong&gt;: LLPY-12 - Docker y Containerización&lt;/p&gt;

&lt;p&gt;En el siguiente post exploraremos cómo containerizar la aplicación con Docker, multi-stage builds, optimización de imágenes, Docker Compose para desarrollo local, y publicación en Docker Hub.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>googlecloud</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>LLPY-09: Phoenix y OpenTelemetry - Observabilidad Completa</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Fri, 17 Oct 2025 18:25:56 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llpy-09-phoenix-y-opentelemetry-observabilidad-completa-2egc</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llpy-09-phoenix-y-opentelemetry-observabilidad-completa-2egc</guid>
      <description>&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Desafío de Debugging en Sistemas RAG&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagina que tu sistema RAG está en producción:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ API recibe 100+ req/s&lt;/li&gt;
&lt;li&gt;✅ Pipeline complejo: Embedding → Qdrant → Reranking → LLM&lt;/li&gt;
&lt;li&gt;✅ Múltiples servicios integrados&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pero hay un problema: &lt;strong&gt;¿Cómo debuggeas cuando algo sale mal?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Preguntas sin Respuesta&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: &lt;em&gt;"¿Por qué esta query tardó 5 segundos cuando debería tomar 2?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality&lt;/strong&gt;: &lt;em&gt;"¿Por qué el LLM generó una respuesta incorrecta?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors&lt;/strong&gt;: &lt;em&gt;"¿Dónde falló exactamente en el pipeline?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: &lt;em&gt;"¿Cuántos tokens estoy consumiendo por día?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Experience&lt;/strong&gt;: &lt;em&gt;"¿Qué % de usuarios obtienen respuestas relevantes?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Sin observabilidad:&lt;/strong&gt; debugging a ciegas con print statements y logs dispersos.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;La Complejidad del RAG Pipeline&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query: "¿Cuántos días de vacaciones?"
   ↓
   ├─ [1] Embedding generation (30ms) ✅
   │   └─ Model: multilingual-e5-small, Dimensions: 384
   ↓
   ├─ [2] Qdrant search (35ms) ✅
   │   └─ Found 10 documents, scores: 0.85-0.91
   ↓
   ├─ [3] Reranking (20ms) ✅
   │   └─ Model: ms-marco-MiniLM, Top-5 kept
   ↓
   ├─ [4] LLM generation (1800ms) ⚠️ SLOW!
   │   └─ OpenAI GPT-3.5-turbo, 1500 tokens
   ↓
   └─ [5] Evaluation (async, 2500ms)
       └─ Relevance: 0.92, Hallucination: 0.05

TOTAL: 1885ms + eval async
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Cómo sabes si cada paso funcionó correctamente?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;La Magnitud del Problema&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Requisitos de Observabilidad para RAG&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;📊 Tracing&lt;/strong&gt;: Visualizar cada paso del pipeline end-to-end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔍 Instrumentation&lt;/strong&gt;: Auto-track llamadas a LLM APIs (OpenAI, Gemini)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📈 Metrics&lt;/strong&gt;: Latency, throughput, error rates por servicio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 Evaluations&lt;/strong&gt;: Calidad automática de respuestas (LLM-as-judge)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔗 Context Propagation&lt;/strong&gt;: Correlacionar logs/spans de una misma request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💰 Cost Tracking&lt;/strong&gt;: Tokens consumidos, costo por query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🐛 Error Tracking&lt;/strong&gt;: Stack traces con contexto completo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 Dashboards&lt;/strong&gt;: Visualización en tiempo real&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Desafíos Técnicos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🕸️ Distributed Tracing&lt;/strong&gt;: Correlacionar operaciones asíncronas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Async Evaluation&lt;/strong&gt;: No bloquear respuesta del usuario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📦 Complex Payloads&lt;/strong&gt;: Serializar metadatos complejos para OTel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Low Overhead&lt;/strong&gt;: Tracing no debe agregar &amp;gt;10ms de latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 Sampling&lt;/strong&gt;: En producción, no trackear todo (costo + overhead)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;La Solución: Phoenix + OpenTelemetry&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es Phoenix?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Phoenix&lt;/strong&gt; (por Arize AI) es una plataforma de observabilidad especializada en LLMs y RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎯 &lt;strong&gt;Purpose-built&lt;/strong&gt;: Diseñado específicamente para aplicaciones LLM&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Tracing&lt;/strong&gt;: Visualización de pipelines RAG completos&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;LLM-as-Judge&lt;/strong&gt;: Evaluación automática de respuestas&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Analytics&lt;/strong&gt;: Métricas de calidad, costo, performance&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;Debugging&lt;/strong&gt;: Drill-down en queries problemáticas&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Open Source&lt;/strong&gt;: Self-hosted o cloud (Phoenix Cloud)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es OpenTelemetry?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OpenTelemetry&lt;/strong&gt; es el estándar de observabilidad open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📏 &lt;strong&gt;Specification&lt;/strong&gt;: Define cómo instrumentar aplicaciones&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;SDKs&lt;/strong&gt;: Librerías para Python, JS, Go, Java, etc.&lt;/li&gt;
&lt;li&gt;🔌 &lt;strong&gt;Exporters&lt;/strong&gt;: Envía datos a backends (Phoenix, Datadog, New Relic, etc.)&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Vendor-neutral&lt;/strong&gt;: No lock-in a un provider específico&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Arquitectura Phoenix + OpenTelemetry&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                    Lus Laboris API                      │
│                                                         │
│  ┌──────────────────────────────────────────────┐     │
│  │  PhoenixMonitoringService                    │     │
│  │  - Tracer Provider (OpenTelemetry)           │     │
│  │  - Instrumentors (OpenAI auto-instrumented)  │     │
│  │  - Custom Spans (manual tracking)            │     │
│  └───────────────┬──────────────────────────────┘     │
│                  │                                      │
│          OpenTelemetry SDK                              │
│                  │                                      │
└──────────────────┼──────────────────────────────────────┘
                   │
                   │ gRPC (4317) o HTTP (6006)
                   │
           ┌───────▼────────┐
           │  Phoenix       │
           │  Collector     │
           │                │
           │  - Receives    │
           │    traces      │
           │  - Stores      │
           │    spans       │
           │  - Runs        │
           │    evals       │
           └───────┬────────┘
                   │
           ┌───────▼────────┐
           │  Phoenix UI    │
           │  (Port 6006)   │
           │                │
           │  - Traces      │
           │  - Metrics     │
           │  - Evals       │
           │  - Analytics   │
           └────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Configuración Paso a Paso&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Variables de Entorno&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env

# Phoenix Configuration
API_PHOENIX_ENABLED=true
API_PHOENIX_ENDPOINT=http://localhost:6006      # Phoenix HTTP
API_PHOENIX_GRPC_ENDPOINT=localhost:4317        # Phoenix gRPC (más rápido)
API_PHOENIX_USE_GRPC=true                       # Preferir gRPC
API_PHOENIX_API_KEY=                             # Vacío para local, requerido para cloud
API_PHOENIX_PROJECT_NAME=lus-laboris-api

# Environment
API_ENVIRONMENT=development  # Options: development, production, testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuraciones por ambiente:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Development&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API_PHOENIX_ENABLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API_PHOENIX_ENDPOINT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:6006&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://app.phoenix.arize.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API_PHOENIX_USE_GRPC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API_PHOENIX_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(vacío)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;phx_...&lt;/code&gt; (cloud API key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;API_ENVIRONMENT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;development&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;production&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Gestión de Servicios con Bash Script&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El proyecto separa los servicios externos de la API para mejor gestión:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estructura:&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;services/
├── vectordb/
│   └── docker-compose.yml    # Qdrant (puertos 6333, 6334)
├── monitoring/
│   └── docker-compose.yml    # Phoenix (puertos 6006, 4317, 9090)
└── manage_services.sh         # Script interactivo de gestión

src/lus_laboris_api/
└── docker-compose.yml         # API (separada)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Script de gestión de servicios:&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="nb"&gt;cd &lt;/span&gt;services

&lt;span class="c"&gt;# Hacer ejecutable (primera vez)&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x manage_services.sh

&lt;span class="c"&gt;# Ejecutar script interactivo&lt;/span&gt;
./manage_services.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Menú interactivo:&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;================================
  Script de Gestión de Servicios
================================

Servicios disponibles:
  1. Qdrant (Base de Datos Vectorial)
  2. Phoenix (Monitoreo y Observabilidad)
  3. Todos los Servicios

Acciones:
  s) Iniciar servicio
  t) Detener servicio
  r) Reiniciar servicio
  l) Mostrar logs
  k) Mostrar estado
  q) Salir

Estado actual:
  ● Qdrant: Ejecutándose
  ● Phoenix: Ejecutándose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inicio rápido (comandos directos):&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;# Iniciar Qdrant&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;services/vectordb
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Iniciar Phoenix&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;services/monitoring
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verificar servicios&lt;/span&gt;
curl http://localhost:6333      &lt;span class="c"&gt;# Qdrant&lt;/span&gt;
curl http://localhost:6006      &lt;span class="c"&gt;# Phoenix&lt;/span&gt;

&lt;span class="c"&gt;# Acceder a UIs&lt;/span&gt;
open http://localhost:6333/dashboard  &lt;span class="c"&gt;# Qdrant Dashboard&lt;/span&gt;
open http://localhost:6006            &lt;span class="c"&gt;# Phoenix UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ventajas de esta arquitectura:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Separación de responsabilidades&lt;/strong&gt;: Servicios independientes de la API&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Gestión simplificada&lt;/strong&gt;: Script bash interactivo en español&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Reutilizable&lt;/strong&gt;: Servicios pueden usarse con otras aplicaciones&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Desarrollo flexible&lt;/strong&gt;: Levantar solo lo que necesitas&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2.1. Opción: Docker Compose Consolidado (Sugerencia)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Para desarrollo local rápido, puedes crear un &lt;code&gt;docker-compose.yml&lt;/code&gt; consolidado en la raíz del proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.dev.yml (en raíz del proyecto)&lt;/span&gt;
&lt;span class="c1"&gt;# Archivo opcional para levantar todo el stack en desarrollo&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Qdrant vector database&lt;/span&gt;
  &lt;span class="na"&gt;qdrant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qdrant/qdrant:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qdrant&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6333:6333"&lt;/span&gt;  &lt;span class="c1"&gt;# HTTP API&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6334:6334"&lt;/span&gt;  &lt;span class="c1"&gt;# gRPC API&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;qdrant_storage:/qdrant/storage&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QDRANT__SERVICE__HTTP_PORT=6333&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QDRANT__SERVICE__GRPC_PORT=6334&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QDRANT__LOG_LEVEL=INFO&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dev-network&lt;/span&gt;

  &lt;span class="c1"&gt;# Phoenix observability&lt;/span&gt;
  &lt;span class="na"&gt;phoenix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arizephoenix/phoenix:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;phoenix&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6006:6006"&lt;/span&gt;  &lt;span class="c1"&gt;# HTTP UI&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4317:4317"&lt;/span&gt;  &lt;span class="c1"&gt;# gRPC collector&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090:9090"&lt;/span&gt;  &lt;span class="c1"&gt;# Prometheus&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PHOENIX_WORKING_DIR=/mnt/data&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;phoenix_data:/mnt/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dev-network&lt;/span&gt;

  &lt;span class="c1"&gt;# Lus Laboris API (opcional)&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/lus_laboris_api&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lus-laboris-api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Phoenix config&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_PHOENIX_ENABLED=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_PHOENIX_ENDPOINT=http://phoenix:6006&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_PHOENIX_GRPC_ENDPOINT=phoenix:4317&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_PHOENIX_USE_GRPC=true&lt;/span&gt;
      &lt;span class="c1"&gt;# Qdrant config&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_QDRANT_URL=http://qdrant:6333&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_QDRANT_PREFER_GRPC=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_QDRANT_GRPC_PORT=6334&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;qdrant&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;phoenix&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dev-network&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;qdrant_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;phoenix_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dev-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Uso del stack consolidado:&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;# Levantar todo el stack (Qdrant + Phoenix + API)&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Solo servicios externos (sin API)&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up &lt;span class="nt"&gt;-d&lt;/span&gt; qdrant phoenix

&lt;span class="c"&gt;# Ver logs&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml logs &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Detener todo&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verificar servicios:&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;# Qdrant&lt;/span&gt;
curl http://localhost:6333
open http://localhost:6333/dashboard

&lt;span class="c"&gt;# Phoenix&lt;/span&gt;
curl http://localhost:6006
open http://localhost:6006

&lt;span class="c"&gt;# API (si está incluida)&lt;/span&gt;
curl http://localhost:8000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;¿Cuándo usar cada opción?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Escenario&lt;/th&gt;
&lt;th&gt;Usar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Desarrollo diario&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Script &lt;code&gt;manage_services.sh&lt;/code&gt; (servicios) + run API local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing rápido&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker-compose.dev.yml&lt;/code&gt; (todo el stack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Desarrollo API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solo &lt;code&gt;services/&lt;/code&gt; (Qdrant + Phoenix) + run API en IDE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Producción&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Servicios separados en GCP (Cloud Run + Compute Engine)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Ventajas del docker-compose consolidado:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Un solo comando&lt;/strong&gt; levanta todo&lt;/li&gt;
&lt;li&gt;🔗 &lt;strong&gt;Networking automático&lt;/strong&gt; entre servicios&lt;/li&gt;
&lt;li&gt;🧹 &lt;strong&gt;Cleanup fácil&lt;/strong&gt; con un solo &lt;code&gt;down&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Ideal para demos&lt;/strong&gt; y onboarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Desventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔄 &lt;strong&gt;Menos flexible&lt;/strong&gt; (no puedes actualizar un servicio sin afectar otros)&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Más recursos&lt;/strong&gt; (levanta todo siempre)&lt;/li&gt;
&lt;li&gt;🐛 &lt;strong&gt;Debugging más complejo&lt;/strong&gt; (logs mezclados)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Settings con Pydantic&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/lus_laboris_api/api/config.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_settings&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseSettings&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseSettings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Application settings&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Phoenix Monitoring Configuration
&lt;/span&gt;    &lt;span class="n"&gt;api_phoenix_enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;api_phoenix_endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;api_phoenix_grpc_endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:4317&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;api_phoenix_use_grpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;api_phoenix_api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;api_phoenix_project_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lus-laboris-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Environment Configuration
&lt;/span&gt;    &lt;span class="n"&gt;api_environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;development&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# development, production, testing
&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;env_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;case_sensitive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;4. PhoenixMonitoringService&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El servicio se inicializa al arrancar la aplicación y configura el stack completo de observabilidad:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hace &lt;code&gt;PhoenixMonitoringService&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lee configuración&lt;/strong&gt; de &lt;code&gt;settings&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project name, endpoints (gRPC/HTTP), API keys&lt;/li&gt;
&lt;li&gt;Detecta ambiente (development vs production)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conecta a Phoenix&lt;/strong&gt; con estrategia inteligente&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prioridad 1&lt;/strong&gt;: gRPC (2-3x más rápido que HTTP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback&lt;/strong&gt;: HTTP si gRPC no está disponible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt;: Si falla, API sigue funcionando&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Registra tracer provider&lt;/strong&gt; con &lt;code&gt;phoenix.otel.register()&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;project_name&lt;/code&gt;: Para separar proyectos en Phoenix UI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auto_instrument=True&lt;/code&gt;: Tracking automático de librerías&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;batch=True&lt;/code&gt; en producción: Agrupa spans para eficiencia&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Auto-instrumenta OpenAI&lt;/strong&gt; con &lt;code&gt;OpenInference&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI: Auto-tracked (tokens, latency, errors)&lt;/li&gt;
&lt;li&gt;Gemini: Manual tracking (no hay instrumentor aún)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crea tracer&lt;/strong&gt; para tracking manual con OpenTelemetry&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Configuración según ambiente:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Development&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Span Processor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SimpleSpanProcessor (inmediato)&lt;/td&gt;
&lt;td&gt;BatchSpanProcessor (agrupa 512 spans)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gRPC o HTTP&lt;/td&gt;
&lt;td&gt;gRPC preferido (performance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt; (debugging)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; (eficiencia)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Beneficios clave:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;gRPC&lt;/strong&gt;: &amp;lt;2ms overhead vs 5-10ms con HTTP&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto-instrumentation&lt;/strong&gt;: OpenAI tracked sin código extra&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Graceful&lt;/strong&gt;: Si Phoenix falla, API no se rompe&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Environment-aware&lt;/strong&gt;: Optimizado por ambiente&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔍 &lt;strong&gt;Tracking del Pipeline RAG&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Session Management&lt;/strong&gt;
&lt;/h3&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;create_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create a monitoring session for a user request&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_tracker&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;]&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;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_calls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metrics&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="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Created monitoring session: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&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="nb"&gt;dict&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;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;End session and calculate final metrics&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_tracker&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="n"&gt;session_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_tracker&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_time&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&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="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_time&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;session_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_time&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;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Calculate final metrics
&lt;/span&gt;    &lt;span class="n"&gt;session_metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_calculate_session_metrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_metrics&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;session_metrics&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Session &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ended. Duration: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;duration&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="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;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Cleanup
&lt;/span&gt;    &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_tracker&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_id&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;session_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Track Embedding Generation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Este método registra el paso de generación de embeddings en el pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué registra?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modelo usado (&lt;code&gt;multilingual-e5-small&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Longitud del texto de entrada&lt;/li&gt;
&lt;li&gt;Tiempo de generación (típicamente ~30ms)&lt;/li&gt;
&lt;li&gt;Session ID para correlación&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cómo funciona:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crea un &lt;strong&gt;span&lt;/strong&gt; de OpenTelemetry llamado &lt;code&gt;"embedding_generation"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Añade &lt;strong&gt;attributes&lt;/strong&gt; (modelo, tiempo, longitud de texto)&lt;/li&gt;
&lt;li&gt;Registra en &lt;strong&gt;session tracker&lt;/strong&gt; local&lt;/li&gt;
&lt;li&gt;Marca como &lt;code&gt;Status.OK&lt;/code&gt; si todo sale bien&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Visible en Phoenix UI:&lt;/strong&gt; Como un span dentro del trace completo del request.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Track Vectorstore Search&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Registra la búsqueda en Qdrant (vectorstore):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué registra?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Longitud de la query&lt;/li&gt;
&lt;li&gt;Cantidad de documentos retornados (típicamente 5-10)&lt;/li&gt;
&lt;li&gt;Tiempo de búsqueda (~30-35ms)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Visible en Phoenix:&lt;/strong&gt; Span &lt;code&gt;"vectorstore_search"&lt;/code&gt; que muestra performance de Qdrant.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Track Reranking&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Registra el paso de reranking con cross-encoder:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué registra?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cantidad de documentos rerankeados (10 → 5)&lt;/li&gt;
&lt;li&gt;Tiempo de reranking (~20ms)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Visible en Phoenix:&lt;/strong&gt; Span &lt;code&gt;"document_reranking"&lt;/code&gt; que muestra overhead del reranking.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Track LLM Call&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Registra llamadas a LLMs (OpenAI o Gemini):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué registra?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider y modelo (&lt;code&gt;openai/gpt-3.5-turbo&lt;/code&gt; o &lt;code&gt;gemini/gemini-1.5-flash&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Longitud de prompt y respuesta&lt;/li&gt;
&lt;li&gt;Tokens consumidos (para cálculo de costos)&lt;/li&gt;
&lt;li&gt;Métricas básicas de calidad (coherence, relevance, completeness)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Auto-instrumentation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt;: Tracked automáticamente por OpenInference (tokens, latency, errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt;: Tracking manual porque no hay instrumentor aún&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Visible en Phoenix:&lt;/strong&gt; Span &lt;code&gt;"llm_call_openai_gpt-3.5-turbo"&lt;/code&gt; que domina el tiempo total (~1.8s de 2s total).&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Integración con RAG Pipeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;En el RAGService&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/lus_laboris_api/api/services/rag_service.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.phoenix_service&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;phoenix_service&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;answer_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&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;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Complete RAG pipeline with Phoenix tracking&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Create session
&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;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phoenix_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_session&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="c1"&gt;# 2. Retrieve documents (tracked internally)
&lt;/span&gt;        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieval_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_retrieve_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 3. Generate response (tracked internally)
&lt;/span&gt;        &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_generate_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 4. Calculate total time
&lt;/span&gt;        &lt;span class="n"&gt;processing_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;

        &lt;span class="c1"&gt;# 5. Enqueue evaluation (async, non-blocking)
&lt;/span&gt;        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;evaluation_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;metadata&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;processing_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;processing_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_model&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;span class="c1"&gt;# 6. Return response
&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;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing_time_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processing_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&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="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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&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;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] Failed to answer question&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;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# End session
&lt;/span&gt;        &lt;span class="n"&gt;phoenix_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&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;Flujo completo:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create session&lt;/li&gt;
&lt;li&gt;Track embedding → Track Qdrant search → Track reranking → Track LLM&lt;/li&gt;
&lt;li&gt;Enqueue async evaluation&lt;/li&gt;
&lt;li&gt;Return response to user&lt;/li&gt;
&lt;li&gt;End session (calculates metrics)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;Visualización en Phoenix UI&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Acceder a Phoenix&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local&lt;/span&gt;
open http://localhost:6006

&lt;span class="c"&gt;# Cloud&lt;/span&gt;
open https://app.phoenix.arize.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Traces View&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Muestra todos los requests con breakdown detallado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────┐
│ Trace: session_abc123 (1.8s)                          │
│                                                        │
│ ├─ embedding_generation (30ms)                        │
│ │  ├─ model: multilingual-e5-small                   │
│ │  └─ text_length: 45                                │
│                                                        │
│ ├─ vectorstore_search (35ms)                          │
│ │  ├─ results_count: 10                              │
│ │  └─ search_time: 0.035s                            │
│                                                        │
│ ├─ document_reranking (20ms)                          │
│ │  ├─ documents_count: 10                            │
│ │  └─ reranking_time: 0.020s                         │
│                                                        │
│ └─ llm_call_openai_gpt-3.5-turbo (1.7s)               │
│    ├─ provider: openai                                │
│    ├─ model: gpt-3.5-turbo                           │
│    ├─ prompt_length: 1450                            │
│    ├─ response_length: 320                           │
│    ├─ quality.coherence: 0.85                        │
│    ├─ quality.relevance: 0.92                        │
│    └─ quality.completeness: 0.78                     │
│                                                        │
│ TOTAL: 1.815s                                          │
└────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3. LLM Calls Dashboard&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│ LLM Calls Summary (Last 24h)                        │
│                                                     │
│ Total Calls: 1,234                                  │
│ OpenAI: 856 (69%)                                   │
│ Gemini: 378 (31%)                                   │
│                                                     │
│ Avg Latency: 1.8s                                   │
│ p50: 1.5s | p95: 2.8s | p99: 4.2s                  │
│                                                     │
│ Total Tokens: 2.3M                                  │
│ Input: 1.5M | Output: 800K                         │
│                                                     │
│ Estimated Cost: $3.45                               │
│ OpenAI: $2.80 | Gemini: $0.65                      │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;4. Evaluations View&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│ Evaluation Results (Last 100 queries)              │
│                                                     │
│ Relevance (avg):    0.89 ████████████████░░ 89%   │
│ Correctness (avg):  0.85 ██████████████░░░░ 85%   │
│ Completeness (avg): 0.78 █████████████░░░░░ 78%   │
│ Hallucination rate:  5%  ░░░░░░░░░░░░░░░░░░  5%   │
│ Grounding (avg):    0.92 ████████████████▓░ 92%   │
│                                                     │
│ Top Issues:                                         │
│ - 8 queries with low completeness (&amp;lt;0.6)           │
│ - 3 potential hallucinations detected              │
│ - 2 queries with poor grounding (&amp;lt;0.7)             │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🤖 &lt;strong&gt;LLM-as-a-Judge Evaluations&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Evaluation Service: Calidad Automática&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El &lt;code&gt;EvaluationService&lt;/code&gt; evalúa la calidad de cada respuesta usando un LLM como juez:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo funciona?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enqueue evaluation&lt;/strong&gt; (no bloqueante)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usuario recibe respuesta inmediatamente&lt;/li&gt;
&lt;li&gt;Evaluación se ejecuta en background thread&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Evalúa múltiples dimensiones&lt;/strong&gt; con prompts específicos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relevance&lt;/strong&gt;: ¿La respuesta es relevante a la pregunta? (0-1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correctness&lt;/strong&gt;: ¿Es factualmente correcta según el contexto? (0-1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucination&lt;/strong&gt;: ¿Contiene info no presente en el contexto? (0-1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounding&lt;/strong&gt;: Inverso de hallucination (1 - hallucination)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Envía resultados a Phoenix&lt;/strong&gt; con &lt;code&gt;phoenix_service.track_evaluation()&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Métricas visibles en Phoenix UI&lt;/li&gt;
&lt;li&gt;Historial completo para análisis de tendencias&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Métricas típicas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relevance: ~0.89 (89%)&lt;/li&gt;
&lt;li&gt;Correctness: ~0.85 (85%)&lt;/li&gt;
&lt;li&gt;Hallucination: ~0.05 (5%)&lt;/li&gt;
&lt;li&gt;Grounding: ~0.92 (92%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Non-blocking&lt;/strong&gt;: No impacta latencia del usuario&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Automated&lt;/strong&gt;: Evalúa 100% de respuestas sin intervención humana&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Continuous&lt;/strong&gt;: Detecta degradación de calidad en tiempo real&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Actionable&lt;/strong&gt;: Identifica queries problemáticas automáticamente&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Casos de Uso Reales&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Debugging de Latency:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"¿Por qué esta query tardó 5 segundos?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Phoenix UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abrir Phoenix → Traces&lt;/li&gt;
&lt;li&gt;Filtrar por &lt;code&gt;processing_time &amp;gt; 5s&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ver breakdown:

&lt;ul&gt;
&lt;li&gt;Embedding: 30ms ✅&lt;/li&gt;
&lt;li&gt;Qdrant: 35ms ✅&lt;/li&gt;
&lt;li&gt;Reranking: 20ms ✅&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM: 4.8s&lt;/strong&gt; ⚠️ CULPABLE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Acción:&lt;/strong&gt; LLM es el cuello de botella. Opciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cambiar a Gemini 1.5 Flash (más rápido)&lt;/li&gt;
&lt;li&gt;Reducir max_tokens&lt;/li&gt;
&lt;li&gt;Implementar caching de respuestas&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Debugging de Calidad:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"¿Por qué el LLM generó una respuesta incorrecta?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Phoenix UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abrir Phoenix → Evaluations&lt;/li&gt;
&lt;li&gt;Filtrar por &lt;code&gt;correctness &amp;lt; 0.6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ver query específica:

&lt;ul&gt;
&lt;li&gt;Question: "¿Cuántos días de preaviso?"&lt;/li&gt;
&lt;li&gt;Context: [5 documentos irrelevantes]&lt;/li&gt;
&lt;li&gt;Answer: "15 días" (incorrecto)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correctness: 0.4&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Acción:&lt;/strong&gt; El problema es retrieval, no LLM. Opciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mejorar query embedding&lt;/li&gt;
&lt;li&gt;Ajustar top_k o reranking&lt;/li&gt;
&lt;li&gt;Revisar metadatos de documentos&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Cost Optimization:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"¿Cuánto estoy gastando en OpenAI?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Phoenix UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abrir Phoenix → LLM Calls&lt;/li&gt;
&lt;li&gt;Ver dashboard:

&lt;ul&gt;
&lt;li&gt;Total tokens (24h): 2.3M&lt;/li&gt;
&lt;li&gt;GPT-3.5-turbo: $2.80/día&lt;/li&gt;
&lt;li&gt;GPT-4: $12.50/día (70 calls)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Acción:&lt;/strong&gt; GPT-4 es muy costoso. Opciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usar GPT-4 solo para queries complejas&lt;/li&gt;
&lt;li&gt;Implementar clasificador (simple → GPT-3.5, complex → GPT-4)&lt;/li&gt;
&lt;li&gt;Migrar a Gemini 1.5 Pro (60% más barato)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para Quality Monitoring:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"¿Qué % de respuestas tienen hallucinations?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Phoenix UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abrir Phoenix → Evaluations&lt;/li&gt;
&lt;li&gt;Ver dashboard:

&lt;ul&gt;
&lt;li&gt;Hallucination rate: &lt;strong&gt;5%&lt;/strong&gt; (61 de 1,234 queries)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Acción:&lt;/strong&gt; 5% es aceptable, pero revisar casos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 queries con hallucination score &amp;gt;0.8&lt;/li&gt;
&lt;li&gt;Común cuando context es irrelevante&lt;/li&gt;
&lt;li&gt;Agregar guardrail: "No tengo información suficiente"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;El Impacto Transformador&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Antes de Phoenix:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🐛 &lt;strong&gt;Debugging&lt;/strong&gt;: print() statements en 10 archivos diferentes&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Performance&lt;/strong&gt;: "Parece lento, pero no sé por qué"&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost&lt;/strong&gt;: "Creo que gasto $50/mes, pero no estoy seguro"&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Quality&lt;/strong&gt;: "Los usuarios dicen que a veces está mal, pero no sé cuándo"&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Metrics&lt;/strong&gt;: CSV files con logs dispersos&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Después de Phoenix:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🐛 &lt;strong&gt;Debugging&lt;/strong&gt;: Ver trace completo en Phoenix UI en 10 segundos&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Performance&lt;/strong&gt;: "LLM tarda 1.8s avg, p99 es 4.2s"&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost&lt;/strong&gt;: "$3.45/día, OpenAI usa $2.80, Gemini $0.65"&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Quality&lt;/strong&gt;: "89% relevance, 5% hallucination rate"&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Metrics&lt;/strong&gt;: Dashboard en tiempo real con drill-down completo&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas de Mejora:&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Sin Phoenix&lt;/th&gt;
&lt;th&gt;Con Phoenix&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MTTR (Mean Time To Resolution)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-4 horas&lt;/td&gt;
&lt;td&gt;10-30 minutos&lt;/td&gt;
&lt;td&gt;-85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quality Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual sampling (10%)&lt;/td&gt;
&lt;td&gt;Automated (100%)&lt;/td&gt;
&lt;td&gt;+900%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Weekly estimates&lt;/td&gt;
&lt;td&gt;Real-time tracking&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance Insights&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guessing&lt;/td&gt;
&lt;td&gt;Precise breakdown&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;Lecciones Aprendidas&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. gRPC es 2-3x Más Rápido que HTTP&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Phoenix con gRPC tiene latency de &amp;lt;2ms para enviar spans. HTTP puede agregar 5-10ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Batch Processing en Producción es Crítico&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En dev, usar &lt;code&gt;SimpleSpanProcessor&lt;/code&gt; (inmediato). En prod, usar &lt;code&gt;BatchSpanProcessor&lt;/code&gt; (agrupa 512 spans antes de enviar).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Session IDs son el Glue&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Correlacionar todos los spans de una request con un session_id único permite drill-down completo en Phoenix.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Async Evaluation no Bloquea&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Evaluar calidad en background (evaluationservice) no impacta latencia de respuesta al usuario.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Auto-Instrumentation &amp;gt; Manual&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;OpenInference auto-instrumenta OpenAI (tracks tokens, latency, errors). Gemini requiere tracking manual porque no tiene instrumentor aún.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. Serialize Complex Metadata&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;OpenTelemetry solo acepta primitives (str, int, float, bool). Dicts/lists deben serializarse a JSON string.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Propósito Más Grande&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Phoenix + OpenTelemetry no es solo "nice to have" - es el &lt;strong&gt;sistema nervioso&lt;/strong&gt; del RAG pipeline. Sin observabilidad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging es adivinanza&lt;/li&gt;
&lt;li&gt;Performance es misterio&lt;/li&gt;
&lt;li&gt;Quality es anécdota&lt;/li&gt;
&lt;li&gt;Cost es estimación&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con Phoenix + OpenTelemetry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Visibility&lt;/strong&gt;: Ver cada paso del pipeline en tiempo real&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Debuggability&lt;/strong&gt;: Drill-down en queries problemáticas en segundos&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Quality&lt;/strong&gt;: Evaluación automática de 100% de respuestas&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Optimization&lt;/strong&gt;: Identificar cuellos de botella con precisión&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Cost Control&lt;/strong&gt;: Track tokens y costos en tiempo real&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Continuous Improvement&lt;/strong&gt;: Métricas históricas para optimización&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estamos convirtiendo un sistema RAG de "caja negra" a &lt;strong&gt;cristal transparente&lt;/strong&gt;, donde cada operación es visible, medible, y optimizable.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Recursos y Enlaces&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Repositorio del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Documentación Técnica&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix Service&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/services/phoenix_service.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/services/phoenix_service.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation Service&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/services/evaluation_service.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/services/evaluation_service.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG Service&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/services/rag_service.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/services/rag_service.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Recursos Externos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix Docs&lt;/strong&gt;: &lt;a href="https://docs.arize.com/phoenix" rel="noopener noreferrer"&gt;docs.arize.com/phoenix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry Python&lt;/strong&gt;: &lt;a href="https://opentelemetry.io/docs/languages/python/" rel="noopener noreferrer"&gt;opentelemetry.io/docs/languages/python&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenInference&lt;/strong&gt;: &lt;a href="https://github.com/Arize-ai/openinference" rel="noopener noreferrer"&gt;github.com/Arize-ai/openinference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix Cloud&lt;/strong&gt;: &lt;a href="https://app.phoenix.arize.com" rel="noopener noreferrer"&gt;app.phoenix.arize.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Próximo Post&lt;/strong&gt;: LLPY-10 - Autenticación JWT con RSA&lt;/p&gt;

&lt;p&gt;En el siguiente post exploraremos cómo implementar autenticación segura con JWT y claves RSA, generación de tokens, validación en endpoints, y integración con GCP Secret Manager.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>spanish</category>
      <category>devops</category>
      <category>llm</category>
    </item>
    <item>
      <title>LLPY-10: Autenticación JWT con RSA - Seguridad Stateless</title>
      <dc:creator>Jesus Oviedo Riquelme</dc:creator>
      <pubDate>Fri, 17 Oct 2025 18:24:50 +0000</pubDate>
      <link>https://dev.to/jesus_oviedoriquelme_084/llpy-10-autenticacion-jwt-con-rsa-seguridad-stateless-39ho</link>
      <guid>https://dev.to/jesus_oviedoriquelme_084/llpy-10-autenticacion-jwt-con-rsa-seguridad-stateless-39ho</guid>
      <description>&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Desafío de la Autenticación en APIs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Imagina que tu API RAG está en producción:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Endpoints públicos: &lt;code&gt;/api/health&lt;/code&gt;, &lt;code&gt;/api/rag/ask&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;⚠️ Endpoints sensibles: &lt;code&gt;/api/vectorstore/load&lt;/code&gt;, &lt;code&gt;/api/status&lt;/code&gt; (detalles completos)&lt;/li&gt;
&lt;li&gt;⚠️ Endpoints administrativos: &lt;code&gt;/api/vectorstore/delete&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;El problema&lt;/strong&gt;: ¿Cómo proteges los endpoints sensibles sin comprometer la performance o escalabilidad?&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Requisitos de Autenticación&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🔒 Seguro&lt;/strong&gt;: Tokens no falsificables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Rápido&lt;/strong&gt;: Validación en &amp;lt;5ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📈 Escalable&lt;/strong&gt;: Sin estado compartido entre instancias&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Stateless&lt;/strong&gt;: No requiere base de datos de sesiones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌐 Estándar&lt;/strong&gt;: Compatible con cualquier cliente HTTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔑 Rotable&lt;/strong&gt;: Cambio de claves sin downtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⏰ Temporal&lt;/strong&gt;: Tokens con expiración automática&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Opciones de Autenticación&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Método&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Contras&lt;/th&gt;
&lt;th&gt;Escalabilidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session cookies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Familiar, fácil&lt;/td&gt;
&lt;td&gt;Requiere DB/Redis&lt;/td&gt;
&lt;td&gt;Limitada ⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Keys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;td&gt;No expira, difícil rotación&lt;/td&gt;
&lt;td&gt;Media ⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OAuth 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Estándar, robusto&lt;/td&gt;
&lt;td&gt;Complejo setup&lt;/td&gt;
&lt;td&gt;Alta ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWT (HS256)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stateless, rápido&lt;/td&gt;
&lt;td&gt;Secret compartido&lt;/td&gt;
&lt;td&gt;Alta ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JWT (RS256)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stateless, secure&lt;/td&gt;
&lt;td&gt;Requiere key pair&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Muy Alta&lt;/strong&gt; ✅✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Nuestra elección&lt;/strong&gt;: &lt;strong&gt;JWT con RS256 (RSA)&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;La Magnitud del Problema&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Desafíos Técnicos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;🔐 Generación de Claves&lt;/strong&gt;: ¿Cómo generar par RSA seguro?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📝 Claims Management&lt;/strong&gt;: ¿Qué información incluir en el token?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✅ Validación&lt;/strong&gt;: ¿Cómo validar firma sin secret compartido?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Distribución&lt;/strong&gt;: ¿Cómo distribuir public key a la API?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⏰ Expiración&lt;/strong&gt;: ¿Cómo manejar tokens expirados?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔁 Renovación&lt;/strong&gt;: ¿Cómo implementar refresh tokens?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🚫 Revocación&lt;/strong&gt;: ¿Cómo invalidar tokens antes de expiración?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;JWT vs Traditional Sessions&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Traditional Sessions:&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;Client → API → Check session in Redis/DB (20-50ms)
         ↓
       Response

Scaling: N servers = N connections to Redis
Cost: Redis instance + network latency
SPOF: Redis down = all auth fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JWT (Stateless):&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;Client → API → Validate signature locally (&amp;lt;5ms)
         ↓
       Response

Scaling: N servers = 0 shared state
Cost: CPU for validation (negligible)
SPOF: None (cada server valida independientemente)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 &lt;strong&gt;La Solución: JWT con RSA (RS256)&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es JWT?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;JWT (JSON Web Token)&lt;/strong&gt; es un estándar (RFC 7519) para tokens de acceso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt;: Algoritmo y tipo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt;: Claims (datos del usuario)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt;: Firma criptográfica&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Por Qué RS256 (RSA)?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HS256 (HMAC)&lt;/strong&gt; vs &lt;strong&gt;RS256 (RSA)&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;HS256&lt;/th&gt;
&lt;th&gt;RS256&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Algoritmo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HMAC + SHA256 (symmetric)&lt;/td&gt;
&lt;td&gt;RSA + SHA256 (asymmetric)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claves&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 secret compartido&lt;/td&gt;
&lt;td&gt;Private + Public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Seguridad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secret debe estar en todos los servers&lt;/td&gt;
&lt;td&gt;Solo public key en API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compromiso&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Si leak = regenerar tokens de todos&lt;/td&gt;
&lt;td&gt;Si leak public key = no importa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uso&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pequeña escala, single app&lt;/td&gt;
&lt;td&gt;Multi-service, microservices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nuestra elección&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;RS256&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;RS256 permite&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Private key&lt;/strong&gt; en un solo lugar (token generation service)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Public key&lt;/strong&gt; en múltiples APIs (stateless validation)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero trust&lt;/strong&gt;: APIs solo validan, nunca generan tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Implementación Paso a Paso&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Generación de Claves RSA&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El script &lt;code&gt;utils/generate_jwt_keys.sh&lt;/code&gt; genera el par de claves RSA necesarias para JWT:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hace el script?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Genera &lt;strong&gt;clave privada&lt;/strong&gt; RSA de 2048 bits con &lt;code&gt;openssl genrsa&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extrae &lt;strong&gt;clave pública&lt;/strong&gt; de la privada con &lt;code&gt;openssl rsa -pubout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Establece &lt;strong&gt;permisos seguros&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Private key: &lt;code&gt;600&lt;/code&gt; (solo owner puede leer/escribir)&lt;/li&gt;
&lt;li&gt;Public key: &lt;code&gt;644&lt;/code&gt; (todos pueden leer)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Guarda en directorio &lt;code&gt;keys/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Ejecutar:&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="nb"&gt;cd &lt;/span&gt;utils
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x generate_jwt_keys.sh
./generate_jwt_keys.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output del script:&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;[INFO] Generando clave privada RSA de 2048 bits...
[SUCCESS] Clave privada generada: keys/private_key.pem
[INFO] Generando clave pública RSA...
[SUCCESS] Clave pública generada: keys/public_key.pem
[SUCCESS] Permisos de archivos configurados correctamente

Información de las claves generadas:
==================================
Clave Privada:
  Archivo: keys/private_key.pem
  Tamaño: 1.7K
  Permisos: -rw------- (600)

Clave Pública:
  Archivo: keys/public_key.pem
  Tamaño: 451B
  Permisos: -rw-r--r-- (644)

Notas importantes:
  - La clave privada debe mantenerse segura y no compartirse
  - La clave pública puede ser compartida para validación de tokens
  - NO versionar private_key.pem en Git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Estructura generada:&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;keys/
├── private_key.pem    # 🔒 Para generar tokens (NUNCA compartir)
└── public_key.pem     # 🔓 Para validar tokens (OK compartir)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Formato de las claves:&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;-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA2Z7X3Y+9QvH5xK...
-----END RSA PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ...
-----END PUBLIC KEY-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Generación de Tokens JWT&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El script &lt;code&gt;utils/generate_jwt_token.py&lt;/code&gt; usa las claves RSA para generar tokens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hace el script?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Carga la clave privada&lt;/strong&gt; desde &lt;code&gt;keys/private_key.pem&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construye payload&lt;/strong&gt; con claims estándar:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: username&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iss&lt;/code&gt;: "lus-laboris-api" (issuer)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt;: "lus-laboris-client" (audience)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt;: timestamp de expiración&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iat&lt;/code&gt;: timestamp de creación&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firma con RS256&lt;/strong&gt; usando la clave privada&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retorna token&lt;/strong&gt; en formato JWT&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Uso del script:&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;# Generar token (expira en 15 minutos por defecto)&lt;/span&gt;
python utils/generate_jwt_token.py &lt;span class="nt"&gt;--username&lt;/span&gt; admin@example.com

&lt;span class="c"&gt;# Con expiración custom (1440 min = 24 horas)&lt;/span&gt;
python utils/generate_jwt_token.py &lt;span class="nt"&gt;--username&lt;/span&gt; admin@example.com &lt;span class="nt"&gt;--expiry&lt;/span&gt; 1440

&lt;span class="c"&gt;# Con claims adicionales&lt;/span&gt;
python utils/generate_jwt_token.py &lt;span class="nt"&gt;--username&lt;/span&gt; admin@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--claims&lt;/span&gt; &lt;span class="s1"&gt;'{"role": "admin", "permissions": ["read", "write"]}'&lt;/span&gt;

&lt;span class="c"&gt;# Validar token&lt;/span&gt;
python utils/generate_jwt_token.py &lt;span class="nt"&gt;--validate&lt;/span&gt; &lt;span class="nt"&gt;--token&lt;/span&gt; &lt;span class="s2"&gt;"eyJhbGc..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output del script:&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;ℹ️  Clave privada cargada desde: keys/private_key.pem
✅ Token generado exitosamente para usuario: admin@example.com
ℹ️  Token expira en: 2025-10-18 15:30:00 UTC (15 minutos)

🔑 Token JWT Generado Exitosamente!
==================================================
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI...
==================================================

📖 Instrucciones de Uso:
1. Usa este token en el header Authorization:
   Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI...

2. Prueba el token con:
   python generate_jwt_token.py --validate --token "eyJhbGc..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Estructura de un Token JWT:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El token tiene 3 partes separadas por &lt;code&gt;.&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Header].[Payload].[Signature]

Header (base64):
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload (base64):
{
  "sub": "admin@example.com",      // Usuario
  "iss": "lus-laboris-api",        // Emisor
  "aud": "lus-laboris-client",     // Audiencia
  "exp": 1729271800,               // Expira (Unix timestamp)
  "iat": 1729268200                // Creado (Unix timestamp)
}

Signature:
RS256(header + "." + payload, private_key) // Firma RSA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claims importantes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sub&lt;/strong&gt;: Identifica al usuario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iss/aud&lt;/strong&gt;: Previenen uso del token en otros sistemas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exp&lt;/strong&gt;: Auto-expiración (no requiere revocación manual)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iat&lt;/strong&gt;: Para auditoría&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Validación en FastAPI (JWTValidator)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;La clase &lt;code&gt;JWTValidator&lt;/code&gt; en &lt;code&gt;jwt_handler.py&lt;/code&gt; se encarga de validar tokens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hace &lt;code&gt;JWTValidator&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Carga la clave pública&lt;/strong&gt; al inicializar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lee desde &lt;code&gt;settings.api_jwt_public_key_path&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Típicamente: &lt;code&gt;keys/public_key.pem&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Falla temprano si el archivo no existe&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Método &lt;code&gt;validate_token()&lt;/code&gt;&lt;/strong&gt; - El corazón de la validación&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decodifica el token usando la &lt;strong&gt;public key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Valida &lt;strong&gt;signature&lt;/strong&gt; con algoritmo RS256&lt;/li&gt;
&lt;li&gt;Verifica &lt;strong&gt;claims&lt;/strong&gt;: exp, iat, iss, aud&lt;/li&gt;
&lt;li&gt;Retorna &lt;strong&gt;payload&lt;/strong&gt; si todo es válido&lt;/li&gt;
&lt;li&gt;Lanza &lt;code&gt;ValueError&lt;/code&gt; si algo falla&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Método &lt;code&gt;is_token_valid()&lt;/code&gt;&lt;/strong&gt; - Helper no-exception&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retorna &lt;code&gt;True&lt;/code&gt; si válido, &lt;code&gt;False&lt;/code&gt; si inválido&lt;/li&gt;
&lt;li&gt;No lanza excepciones (para checks silenciosos)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Validaciones automáticas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Expiration&lt;/strong&gt; (&lt;code&gt;exp&lt;/code&gt;): Token no expirado&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Issued At&lt;/strong&gt; (&lt;code&gt;iat&lt;/code&gt;): Token ya fue emitido (no futuro)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Issuer&lt;/strong&gt; (&lt;code&gt;iss&lt;/code&gt;): Coincide con &lt;code&gt;settings.api_jwt_iss&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Audience&lt;/strong&gt; (&lt;code&gt;aud&lt;/code&gt;): Coincide con &lt;code&gt;settings.api_jwt_aud&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Signature&lt;/strong&gt;: Firmado con la private key correcta&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Errores manejados:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jwt.ExpiredSignatureError&lt;/code&gt; → "Token has expired"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jwt.InvalidAudienceError&lt;/code&gt; → "Invalid audience"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jwt.InvalidIssuerError&lt;/code&gt; → "Invalid issuer"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jwt.InvalidTokenError&lt;/code&gt; → "Invalid token"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validación completa: &lt;strong&gt;&amp;lt;5ms&lt;/strong&gt; (CPU-bound, muy rápido)&lt;/li&gt;
&lt;li&gt;No requiere llamadas externas (DB, Redis, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Security Dependencies para FastAPI&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El archivo &lt;code&gt;src/lus_laboris_api/api/auth/security.py&lt;/code&gt; proporciona las funciones de autenticación:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué contiene &lt;code&gt;security.py&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SecurityManager class&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Centraliza la lógica de autenticación&lt;/li&gt;
&lt;li&gt;Valida tokens JWT&lt;/li&gt;
&lt;li&gt;Maneja errores de autenticación&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;get_current_user()&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency que requiere JWT válido&lt;/li&gt;
&lt;li&gt;Extrae payload del token&lt;/li&gt;
&lt;li&gt;Retorna información del usuario&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;optional_auth()&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency con autenticación opcional&lt;/li&gt;
&lt;li&gt;Retorna &lt;code&gt;dict&lt;/code&gt; si hay token válido&lt;/li&gt;
&lt;li&gt;Retorna &lt;code&gt;None&lt;/code&gt; si no hay token (no bloquea acceso)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;require_admin()&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency para operaciones administrativas&lt;/li&gt;
&lt;li&gt;Requiere token JWT válido&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;require_vectorstore_write()&lt;/strong&gt; y &lt;strong&gt;require_vectorstore_read()&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependencies para operaciones del vectorstore&lt;/li&gt;
&lt;li&gt;Requieren token JWT válido&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Uso en endpoints:&lt;/strong&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;..auth.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;optional_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;require_admin&lt;/span&gt;

&lt;span class="c1"&gt;# Endpoint protegido (requiere JWT)
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/protected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;protected_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&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;message&lt;/span&gt;&lt;span class="sh"&gt;"&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;Hello &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sub&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="c1"&gt;# Endpoint con auth opcional
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health/detailed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detailed_health&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optional_auth&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;user&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;status&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;healthy&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;details&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;...&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;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Endpoint admin (requiere JWT)
&lt;/span&gt;&lt;span class="nd"&gt;@router.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/collections/{name}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;require_admin&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;deleted_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sub&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;h3&gt;
  
  
  &lt;strong&gt;5. Swagger UI Integration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;La autenticación JWT se integra automáticamente en Swagger UI gracias a &lt;code&gt;HTTPBearer&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flujo en Swagger:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Acceder a Swagger UI&lt;/strong&gt;: &lt;code&gt;http://localhost:8000/docs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click en "Authorize" 🔓&lt;/strong&gt; (botón en la parte superior)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingresar token&lt;/strong&gt;: Pegar el token JWT en el campo "Value"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click "Authorize"&lt;/strong&gt;: El token se guarda en la sesión&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar endpoints&lt;/strong&gt;: Ahora puedes llamar endpoints protegidos&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Características:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto-detecta&lt;/strong&gt; endpoints con &lt;code&gt;Depends(get_current_user)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Candado visible&lt;/strong&gt; 🔒 en endpoints protegidos&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Botón unlock&lt;/strong&gt; para ingresar token&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Persistencia&lt;/strong&gt;: Token se mantiene mientras Swagger esté abierto&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Logout&lt;/strong&gt;: Click en "Logout" para borrar token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ejemplo visual en Swagger:&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;GET /api/rag/ask                   [Sin candado] ← Público
GET /api/health                    [Sin candado] ← Público
GET /api/status                    🔒 [Authorize] ← Protegido
DELETE /api/vectorstore/delete     🔒 [Authorize] ← Protegido
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔐 &lt;strong&gt;Integración con GCP Secret Manager&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Almacenar Public Key en Secret Manager&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Crear secret con la public key&lt;/span&gt;
gcloud secrets create jwt-public-key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keys/public_key.pem &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tu-proyecto-gcp &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--replication-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"automatic"&lt;/span&gt;

&lt;span class="c"&gt;# Verificar&lt;/span&gt;
gcloud secrets versions list jwt-public-key &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tu-proyecto-gcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Montar Secret en Cloud Run&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En el deployment de Cloud Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy lus-laboris-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tu-imagen &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-secrets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/app/secrets/jwt/public_key.pem=jwt-public-key:latest"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"API_JWT_PUBLIC_KEY_PATH=/app/secrets/jwt/public_key.pem"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3. GitHub Actions Workflow&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update JWT Public Key Secret&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "${JWT_PUBLIC_KEY}" | \&lt;/span&gt;
      &lt;span class="s"&gt;gcloud secrets versions add jwt-public-key \&lt;/span&gt;
      &lt;span class="s"&gt;--data-file=- \&lt;/span&gt;
      &lt;span class="s"&gt;--project=${GCP_PROJECT_ID}&lt;/span&gt;

    &lt;span class="s"&gt;echo "✅ JWT public key secret updated"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;Casos de Uso Reales&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Para APIs Públicas con Endpoints Protegidos:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero que /health sea público pero /status sea protegido"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&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="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Public - no auth&lt;/span&gt;&lt;span class="sh"&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;status&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;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Protected - JWT required&lt;/span&gt;&lt;span class="sh"&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;status&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;healthy&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;details&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="c1"&gt;# Info sensible
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requested_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Integración con Aplicaciones Externas:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Otra aplicación necesita cargar datos al vectorstore"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Generar token para la aplicación externa&lt;/span&gt;
python generate_jwt_token.py generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--username&lt;/span&gt; external-app@company.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expiry&lt;/span&gt; 10080  &lt;span class="c"&gt;# 7 días&lt;/span&gt;

&lt;span class="c"&gt;# Compartir token (de forma segura)&lt;/span&gt;
&lt;span class="c"&gt;# La app externa puede hacer requests:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://api/vectorstore/load &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer eyJhbGc..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"filename": "data.json"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Diferentes Niveles de Acceso:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Admin puede delete, user puede read"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&lt;/strong&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;require_admin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Dependency que requiere rol admin&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin access required&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="n"&gt;user&lt;/span&gt;

&lt;span class="nd"&gt;@router.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/collections/{name}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&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;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;require_admin&lt;/span&gt;&lt;span class="p"&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;Solo admins pueden eliminar&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Para Debugging y Testing:&lt;/strong&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Quiero probar endpoints protegidos en desarrollo"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Solución:&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;# Generar token de desarrollo (expira en 24 horas)&lt;/span&gt;
python generate_jwt_token.py generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--username&lt;/span&gt; dev@localhost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expiry&lt;/span&gt; 1440 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; dev_token.txt

&lt;span class="c"&gt;# Usar en requests&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;dev_token.txt&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:8000/api/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;El Impacto Transformador&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Antes de JWT:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🔑 &lt;strong&gt;Session management&lt;/strong&gt;: Redis/DB requerido para sesiones&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Scaling issues&lt;/strong&gt;: N servers = N connections to session store&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Latency&lt;/strong&gt;: 20-50ms para check session&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost&lt;/strong&gt;: Redis instance ($50-100/mes)&lt;/li&gt;
&lt;li&gt;🚨 &lt;strong&gt;SPOF&lt;/strong&gt;: Redis down = all auth fails&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Después de JWT (RS256):&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🔑 &lt;strong&gt;Stateless&lt;/strong&gt;: Cero dependencias externas&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Infinite scaling&lt;/strong&gt;: N servers = 0 shared state&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Ultra-fast&lt;/strong&gt;: &amp;lt;5ms validation (CPU-bound)&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Zero cost&lt;/strong&gt;: CPU cost negligible&lt;/li&gt;
&lt;li&gt;🚨 &lt;strong&gt;No SPOF&lt;/strong&gt;: Cada server valida independientemente&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Métricas de Mejora:&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Sin JWT&lt;/th&gt;
&lt;th&gt;Con JWT (RS256)&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency de validación&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20-50ms&lt;/td&gt;
&lt;td&gt;&amp;lt;5ms&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis/DB&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;-100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost (auth infra)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$50-100/mes&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;-100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SPOF risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 &lt;strong&gt;Lecciones Aprendidas&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. RS256 &amp;gt; HS256 para Multi-Service&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si tu API se comunica con otros servicios, RS256 es mejor porque solo necesitas compartir la public key (no sensitive).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Expiry Time es Crítico&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corto (15-60 min)&lt;/strong&gt;: Más seguro, pero requiere refresh frecuente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Largo (24 horas+)&lt;/strong&gt;: Más convenient, pero mayor ventana de compromiso&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nuestro sweet spot&lt;/strong&gt;: 60 minutos para API calls, 7 días para batch jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Claims Mínimos son Mejores&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Solo incluir en el payload lo que realmente necesitas validar. No uses JWT como session storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Public Key en Secret Manager&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Aunque la public key no es sensible, almacenarla en Secret Manager facilita rotación sin re-deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. &lt;code&gt;optional_auth&lt;/code&gt; es Poderoso&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Permite endpoints que dan más info si estás autenticado, pero no bloquean acceso público.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. Swagger UI con JWT es UX++&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El botón "Authorize" en Swagger UI hace testing de endpoints protegidos trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔐 &lt;strong&gt;Seguridad Best Practices&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Never Version Private Key&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .gitignore
keys/private_key.pem
*.pem
!public_key.pem  # Public key is safe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Secure Key Permissions&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 keys/private_key.pem  &lt;span class="c"&gt;# Owner read/write only&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 keys/public_key.pem   &lt;span class="c"&gt;# Read for everyone (safe)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;3. Rotate Keys Periodically&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate new key pair&lt;/span&gt;
./generate_jwt_keys.sh &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# Deploy new public key to API&lt;/span&gt;
gcloud secrets versions add jwt-public-key &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keys/public_key.pem

&lt;span class="c"&gt;# Gradually phase out old tokens (based on expiry)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;4. Use Short-Lived Tokens&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# For interactive users: 15-60 minutes
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiry_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# For service accounts: 7 days max
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiry_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;5. Validate All Standard Claims&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Always verify: exp, iat, iss, aud
&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;options&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;verify_exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_iat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_aud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_iss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;El Propósito Más Grande&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;JWT con RS256 no es solo autenticación - es &lt;strong&gt;libertad arquitectural&lt;/strong&gt;. Al eliminar la necesidad de estado compartido:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🚀 Scaling&lt;/strong&gt;: Cada instancia de la API es independiente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌐 Multi-region&lt;/strong&gt;: Tokens válidos en cualquier región&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Microservices&lt;/strong&gt;: Diferentes servicios validan con la misma public key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💰 Cost&lt;/strong&gt;: Zero infrastructure para auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Performance&lt;/strong&gt;: Validación local ultra-rápida&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔒 Security&lt;/strong&gt;: Public key leak = no problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Estamos construyendo una API que escala infinitamente sin necesidad de coordinación entre instancias, sin single points of failure, y sin costo adicional de infraestructura.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;🔗 Recursos y Enlaces&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Repositorio del Proyecto&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py" rel="noopener noreferrer"&gt;lus-laboris-py&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Documentación Técnica&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JWT Handler&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/auth/jwt_handler.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/auth/jwt_handler.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Dependencies&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/src/lus_laboris_api/api/auth/security.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/lus_laboris_api/api/auth/security.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate Keys Script&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/utils/generate_jwt_keys.sh" rel="noopener noreferrer"&gt;&lt;code&gt;utils/generate_jwt_keys.sh&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate Token Script&lt;/strong&gt;: &lt;a href="https://github.com/jesusoviedo/lus-laboris-py/blob/main/utils/generate_jwt_token.py" rel="noopener noreferrer"&gt;&lt;code&gt;utils/generate_jwt_token.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Recursos Externos&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JWT.io&lt;/strong&gt;: &lt;a href="https://jwt.io/" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt; - Decode y debug tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 7519 (JWT)&lt;/strong&gt;: &lt;a href="https://tools.ietf.org/html/rfc7519" rel="noopener noreferrer"&gt;tools.ietf.org/html/rfc7519&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyJWT Docs&lt;/strong&gt;: &lt;a href="https://pyjwt.readthedocs.io/" rel="noopener noreferrer"&gt;pyjwt.readthedocs.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSA Cryptography&lt;/strong&gt;: &lt;a href="https://cryptography.io/" rel="noopener noreferrer"&gt;cryptography.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Próximo Post&lt;/strong&gt;: LLPY-11 - Terraform: Infraestructura como Código&lt;/p&gt;

&lt;p&gt;En el siguiente post exploraremos cómo gestionar toda la infraestructura de GCP con Terraform, desde VMs para Qdrant hasta Cloud Run para la API, con módulos reutilizables y CI/CD automatizado.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>architecture</category>
      <category>api</category>
      <category>security</category>
    </item>
  </channel>
</rss>
