1.13 Agentes de Inteligencia Artificial#

GPT + Ollama en Google Colab — ReAct, Reflexion, Memoria, Multi-Agente#


Un agente es una entidad que percibe su entorno y actua sobre el para alcanzar objetivos:

\[a_t = f(h_t), \quad h_t = (p_1,a_1,p_2,a_2,\ldots,p_t)\]

Contenidos:

  1. Setup y configuracion

  2. Agente basico: el bucle percepcion-accion

  3. Herramientas y Function Calling

  4. ReAct: Razonamiento y Accion entrelazados

  5. Reflexion y autocorreccion

  6. Memoria de agentes

  7. Planificacion: CoT y descomposicion de tareas

  8. Sistemas multi-agente

  9. Agentes con Ollama (modelos locales)

  10. Comparativa y guia de seleccion

Antes de empezar: activa GPU T4 en Editar > Configuracion del cuaderno.

# Instalar dependencias
# openai   -> cliente OpenAI (GPT-4o, Function Calling, Embeddings)
# requests -> llamadas HTTP a la API REST de Ollama
# numpy    -> vectores para memoria semantica
# rich     -> impresion formateada

!pip install openai requests numpy rich -q
print('Librerias instaladas.')
Librerias instaladas.
import os, json, time, math, re, subprocess, threading
import numpy as np
import requests
from openai import OpenAI
from collections import defaultdict
from datetime import datetime
from IPython.display import display

# ── CONFIGURACION DE API KEY ──────────────────
# Opcion 1 (segura): Colab Secrets
#   Panel izquierdo > icono llave > agregar OPENAI_API_KEY
# Opcion 2: asignar directamente (no subir a GitHub)
# ─────────────────────────────────────────────
try:
    from google.colab import userdata
    OPENAI_API_KEY = userdata.get('api_key')
    print('API key cargada desde Colab Secrets.')
except Exception:
    OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', 'tu_api_key')
    print('API key cargada desde variable de entorno.')

client = OpenAI(api_key=OPENAI_API_KEY)
DEFAULT_MODEL = 'gpt-4o-mini'   # rapido, economico, soporta Function Calling

# ── UTILIDADES DE IMPRESION ───────────────────
AZUL='\033[94m'; VERDE='\033[92m'; AMARILLO='\033[93m'
ROJO='\033[91m'; MAGENTA='\033[95m'; RESET='\033[0m'; BOLD='\033[1m'

def imprimir_pensamiento(t): print(f'{AZUL}{BOLD}Pensamiento:{RESET} {AZUL}{t}{RESET}')
def imprimir_accion(h,a):    print(f'{AMARILLO}{BOLD}Accion:{RESET} {AMARILLO}{h}({json.dumps(a,ensure_ascii=False)}){RESET}')
def imprimir_observacion(r):
    rs = str(r)[:200]+('...' if len(str(r))>200 else '')
    print(f'{VERDE}{BOLD}Observacion:{RESET} {VERDE}{rs}{RESET}')
def imprimir_respuesta(t):   print(f'{MAGENTA}{BOLD}Respuesta Final:{RESET}\n{MAGENTA}{t}{RESET}')
def imprimir_agente(n,t,c=AZUL): print(f'{c}{BOLD}[{n}]:{RESET} {t}')
def separador(titulo=''):
    linea = '─'*60
    if titulo: print(f'\n{BOLD}{linea}\n  {titulo}\n{linea}{RESET}')
    else:      print(linea)

print('Configuracion lista.')
API key cargada desde variable de entorno.
Configuracion lista.

PARTE 2 — Agente Basico: El Bucle Percepcion-Accion#

El ciclo fundamental de cualquier agente:

\[\text{PERCIBIR} \rightarrow \text{RAZONAR} \rightarrow \text{ACTUAR} \rightarrow \text{OBSERVAR} \rightarrow \cdots\]

El LLM actua como motor de razonamiento: recibe el estado actual (historial completo) y genera la siguiente accion.

class AgenteBase:
    """
    Agente LLM con el bucle percepcion-accion mas simple posible.

    El historial es la 'memoria de trabajo' del agente (ventana de contexto).
    En cada turno:
    1. Añadir la percepcion al historial.
    2. El LLM procesa TODO el historial -> genera accion.
    3. Guardar la accion en el historial para el siguiente ciclo.

    Clave: el LLM NO tiene estado entre llamadas; el historial es la memoria.
    """

    def __init__(self, nombre, system_prompt, modelo=DEFAULT_MODEL, temperatura=0.7):
        self.nombre      = nombre
        self.modelo      = modelo
        self.temperatura = temperatura
        # El system prompt define identidad, capacidades y restricciones del agente
        self.historial   = [{'role': 'system', 'content': system_prompt}]

    def percibir_y_actuar(self, percepcion, max_tokens=500):
        # PERCIBIR: añadir al historial
        self.historial.append({'role': 'user', 'content': percepcion})

        # RAZONAR Y ACTUAR: el LLM procesa el historial completo
        respuesta = client.chat.completions.create(
            model=self.modelo, messages=self.historial,
            temperature=self.temperatura, max_tokens=max_tokens,
        )
        accion = respuesta.choices[0].message.content

        # OBSERVAR: guardar la accion para el siguiente ciclo
        self.historial.append({'role': 'assistant', 'content': accion})
        return accion

    def resetear(self):
        self.historial = [self.historial[0]]

    @property
    def n_turnos(self):
        return (len(self.historial) - 1) // 2


# Demo: agente investigador con historial multi-turno
investigador = AgenteBase(
    nombre='Investigador',
    system_prompt='Eres un investigador de IA. Respondes con precision tecnica. '
                  'Cuando no sabes algo lo dices claramente.'
)

separador('DEMO — Agente Basico')

r1 = investigador.percibir_y_actuar('Explica brevemente la diferencia entre agente reactivo y agente basado en objetivos.')
print(f'[Turno 1]\n{r1}\n')

# El agente recuerda el contexto del turno anterior sin que lo repitamos
r2 = investigador.percibir_y_actuar('Y como encajan los LLMs en esa clasificacion?')
print(f'[Turno 2]\n{r2}\n')

print(f'Mensajes en historial: {len(investigador.historial)} (1 system + {investigador.n_turnos*2} intercambios)')
────────────────────────────────────────────────────────────
  DEMO — Agente Basico
────────────────────────────────────────────────────────────
[Turno 1]
Los agentes reactivos y los agentes basados en objetivos son dos enfoques diferentes en la inteligencia artificial para la toma de decisiones y el comportamiento de los agentes.

1. **Agente Reactivo**: Este tipo de agente toma decisiones basadas en la percepción actual del entorno, sin mantener un estado interno persistente o planificar a largo plazo. Responde directamente a los estímulos que recibe, utilizando reglas o comportamientos predefinidos. Por ejemplo, un robot que evita obstáculos simplemente detectando su presencia y cambiando de dirección en el momento.

2. **Agente Basado en Objetivos**: Este agente, en cambio, tiene metas específicas que desea alcanzar y utiliza un estado interno para representar la información sobre el entorno y sus objetivos. Planifica y toma decisiones basadas en la comparación entre su estado actual y el estado deseado, considerando las acciones que le acercan a sus metas. Por ejemplo, un agente que debe llegar a un destino específico utilizará un plan que considere múltiples pasos o acciones necesarias para lograrlo.

En resumen, los agentes reactivos responden a situaciones inmediatas sin memoria de estado, mientras que los agentes basados en objetivos planifican y actúan en función de metas a largo plazo.

[Turno 2]
Los Modelos de Lenguaje de Gran Escala (LLMs, por sus siglas en inglés) pueden ser clasificados principalmente como agentes reactivos, aunque presentan características que les permiten simular un comportamiento más complejo en ciertas circunstancias.

1. **Agentes Reactivos**: Los LLMs funcionan principalmente generando respuestas basadas en el contexto inmediato de la entrada que reciben. No mantienen un estado interno persistente a lo largo de interacciones prolongadas y no planifican de manera activa. En este sentido, responden a las consultas de los usuarios en función de patrones aprendidos a partir de grandes cantidades de texto, sin tener en cuenta un objetivo a largo plazo más allá de la generación de una respuesta coherente.

2. **Simulación de Agentes Basados en Objetivos**: Aunque los LLMs no son agentes basados en objetivos en el sentido estricto, pueden simular este comportamiento cuando se les proporciona información adicional o directrices sobre objetivos específicos. Por ejemplo, pueden ser configurados para seguir instrucciones que impliquen realizar tareas específicas, como escribir un ensayo o resolver un problema, lo que implica una cierta dirección en sus respuestas. Sin embargo, esto no equivale a un verdadero estado interno o planificación, ya que su funcionamiento sigue siendo reactivo a la entrada.

En resumen, los LLMs se clasifican principalmente como agentes reactivos, pero pueden ser adaptados para seguir ciertos objetivos a corto plazo mediante la formulación adecuada de preguntas o instrucciones. Sin embargo, no poseen una comprensión interna profunda de objetivos en el mismo sentido que un agente basado en objetivos tradicional.

Mensajes en historial: 5 (1 system + 4 intercambios)

PARTE 3 — Herramientas y Function Calling#

Las herramientas extienden al agente mas alla de la generacion de texto:

  1. Definir herramientas con JSON Schema que el LLM puede leer.

  2. El LLM decide que herramienta llamar y con que argumentos.

  3. Nosotros ejecutamos la funcion Python real.

  4. El resultado vuelve al LLM para formular la respuesta final.

class RegistroHerramientas:
    """
    Registro centralizado de herramientas disponibles para el agente.

    Separa la descripcion (para el LLM) de la implementacion (Python).
    Permite añadir/quitar herramientas sin cambiar el bucle del agente.
    """

    def __init__(self):
        self._funciones = {}   # nombre -> funcion Python
        self._esquemas  = []   # esquemas JSON para la API de OpenAI

    def registrar(self, nombre, funcion, descripcion, parametros):
        """
        nombre      : identificador unico
        funcion     : callable Python que implementa la herramienta
        descripcion : texto que el LLM lee para decidir cuando usarla
        parametros  : JSON Schema con los parametros aceptados
        """
        self._funciones[nombre] = funcion
        self._esquemas.append({'type': 'function', 'function': {
            'name': nombre, 'description': descripcion, 'parameters': parametros
        }})

    def ejecutar(self, nombre, argumentos):
        """Ejecuta la herramienta con los argumentos dados por el LLM."""
        if nombre not in self._funciones:
            return f'Error: herramienta "{nombre}" no encontrada.'
        try:
            return self._funciones[nombre](**argumentos)
        except Exception as e:
            return f'Error al ejecutar {nombre}: {str(e)}'

    @property
    def esquemas(self):
        return self._esquemas


# ── IMPLEMENTACION DE HERRAMIENTAS ─────────────────────────────
# En produccion llamarian a APIs reales; aqui usan datos simulados.

def buscar_web(query, n_resultados=3):
    """Simula busqueda en internet."""
    base = {
        'agentes': [
            {'titulo': 'AutoGPT', 'resumen': 'Agente autonomo basado en GPT-4 que descompone y ejecuta tareas complejas.'},
            {'titulo': 'LangChain Agents', 'resumen': 'Framework para construir agentes LLM con herramientas y memoria.'},
            {'titulo': 'ReAct (2022)', 'resumen': 'Entrelaza razonamiento y accion en LLMs para tareas de busqueda.'},
        ],
        'llm': [
            {'titulo': 'GPT-4 Technical Report', 'resumen': 'Modelo multimodal de OpenAI que supera el 90% en el bar exam.'},
            {'titulo': 'Llama 3.1', 'resumen': 'Modelo abierto de Meta con 405B parametros comparable a GPT-4.'},
        ],
        'react': [
            {'titulo': 'ReAct: Synergizing Reasoning and Acting', 'resumen': 'Yao et al. 2022. Pensamiento+Accion+Observacion mejora tareas multi-paso.'},
        ],
    }
    for key in base:
        if key in query.lower():
            return base[key][:n_resultados]
    return [{'titulo': f'Resultados: {query}', 'resumen': f'Informacion sobre {query}.'}]

def calcular(expresion):
    """Evalua expresiones matematicas de forma segura."""
    import ast
    try:
        nodos_ok = {ast.Expression,ast.Constant,ast.BinOp,ast.UnaryOp,
                    ast.Add,ast.Sub,ast.Mult,ast.Div,ast.Pow,ast.Mod,ast.USub}
        tree = ast.parse(expresion, mode='eval')
        for n in ast.walk(tree):
            if type(n) not in nodos_ok:
                return 'Expresion no permitida.'
        return {'expresion': expresion, 'resultado': round(eval(compile(tree,'','eval')),6)}
    except Exception as e:
        return f'Error: {e}'

def ejecutar_python(codigo):
    """Ejecuta codigo Python y captura la salida. USAR SANDBOX EN PRODUCCION."""
    import io, contextlib
    buf = io.StringIO()
    builtins_seguros = {
        '__builtins__': {
            'print':print,'range':range,'len':len,'sum':sum,'max':max,'min':min,
            'sorted':sorted,'enumerate':enumerate,'zip':zip,'list':list,'dict':dict,
            'str':str,'int':int,'float':float,'abs':abs,'round':round,
        }
    }
    try:
        with contextlib.redirect_stdout(buf):
            exec(codigo, builtins_seguros)
        return buf.getvalue() or 'Codigo ejecutado sin salida.'
    except Exception as e:
        return f'Error: {e}'

def obtener_hora_fecha():
    ahora = datetime.now()
    return {'fecha': ahora.strftime('%Y-%m-%d'), 'hora': ahora.strftime('%H:%M:%S'),
            'dia_semana': ahora.strftime('%A')}

def leer_base_datos(tabla, filtro=None):
    db = {
        'proyectos': [
            {'id':1,'nombre':'AgenteSalud','estado':'activo','presupuesto':150000},
            {'id':2,'nombre':'ChatbotLegal','estado':'pausado','presupuesto':80000},
            {'id':3,'nombre':'RAG-Docs','estado':'activo','presupuesto':60000},
        ],
        'empleados': [
            {'id':1,'nombre':'Ana Lopez','rol':'ML Engineer','proyecto_id':1},
            {'id':2,'nombre':'Luis Mora','rol':'Investigador','proyecto_id':3},
            {'id':3,'nombre':'Sara Ruiz','rol':'Product Manager','proyecto_id':1},
        ],
    }
    datos = db.get(tabla, [])
    if filtro:
        k, v = list(filtro.items())[0]
        datos = [d for d in datos if str(d.get(k,''))==str(v)]
    return datos

# Crear registro y añadir herramientas
registro = RegistroHerramientas()
registro.registrar('buscar_web', buscar_web, 'Busca informacion en internet.',
    {'type':'object','properties':{
        'query':{'type':'string','description':'Termino de busqueda'},
        'n_resultados':{'type':'integer','description':'Cantidad de resultados','default':3}
    },'required':['query']})
registro.registrar('calcular', calcular, 'Calcula expresiones matematicas (+,-,*,/,**,%).',
    {'type':'object','properties':{'expresion':{'type':'string'}},'required':['expresion']})
registro.registrar('ejecutar_python', ejecutar_python, 'Ejecuta codigo Python y retorna el print.',
    {'type':'object','properties':{'codigo':{'type':'string'}},'required':['codigo']})
registro.registrar('obtener_hora_fecha', obtener_hora_fecha, 'Retorna la fecha y hora actual.',
    {'type':'object','properties':{}})
registro.registrar('leer_base_datos', leer_base_datos, 'Consulta la base de datos de la empresa.',
    {'type':'object','properties':{
        'tabla':{'type':'string','enum':['proyectos','empleados']},
        'filtro':{'type':'object','description':'Filtro opcional {campo: valor}'}
    },'required':['tabla']})

print('Herramientas:', [e['function']['name'] for e in registro.esquemas])
Herramientas: ['buscar_web', 'calcular', 'ejecutar_python', 'obtener_hora_fecha', 'leer_base_datos']
def agente_con_herramientas(tarea, registro, modelo=DEFAULT_MODEL,
                             max_pasos=10, verbose=True):
    """
    Agente con Function Calling de OpenAI.

    Bucle:
    1. Enviar mensajes + herramientas al LLM.
    2. Si finish_reason='tool_calls': ejecutar herramienta(s) y continuar.
    3. Si finish_reason='stop': respuesta final, terminar.

    El LLM puede llamar MULTIPLES herramientas en un mismo turno (paralelo).
    Y puede necesitar MULTIPLES turnos (secuencial) para tareas complejas.
    """
    mensajes = [
        {'role': 'system', 'content':
         'Eres un asistente inteligente con herramientas. '
         'Usa las herramientas necesarias y da una respuesta clara y completa.'},
        {'role': 'user', 'content': tarea}
    ]
    if verbose: separador(f'TAREA: {tarea}')

    for paso in range(max_pasos):
        respuesta = client.chat.completions.create(
            model=modelo, messages=mensajes,
            tools=registro.esquemas,
            tool_choice='auto',   # 'auto': el LLM decide; 'none': fuerza texto; {function:{name:...}}: fuerza tool
        )
        msg   = respuesta.choices[0].message
        razon = respuesta.choices[0].finish_reason

        mensajes.append(msg)  # añadir respuesta del LLM al historial

        if razon == 'stop':
            # El LLM decidio que tiene suficiente informacion
            if verbose: imprimir_respuesta(msg.content)
            return msg.content

        if razon == 'tool_calls':
            for llamada in msg.tool_calls:
                nombre = llamada.function.name
                args   = json.loads(llamada.function.arguments)
                if verbose: imprimir_accion(nombre, args)

                resultado = registro.ejecutar(nombre, args)
                if verbose: imprimir_observacion(resultado)

                # role='tool' es el rol especial para resultados de herramientas
                # tool_call_id vincula este resultado con su llamada especifica
                mensajes.append({
                    'role': 'tool',
                    'tool_call_id': llamada.id,
                    'content': json.dumps(resultado, ensure_ascii=False, default=str)
                })

    return 'Max pasos alcanzados.'


# Prueba 1: multiples herramientas en una sola tarea
agente_con_herramientas(
    'Busca informacion sobre agentes de IA, calcula 2**20 y dime que hora es.',
    registro
)
────────────────────────────────────────────────────────────
  TAREA: Busca informacion sobre agentes de IA, calcula 2**20 y dime que hora es.
────────────────────────────────────────────────────────────
Accion: buscar_web({"query": "agentes de IA", "n_resultados": 3})
Observacion: [{'titulo': 'AutoGPT', 'resumen': 'Agente autonomo basado en GPT-4 que descompone y ejecuta tareas complejas.'}, {'titulo': 'LangChain Agents', 'resumen': 'Framework para construir agentes LLM con her...
Accion: calcular({"expresion": "2**20"})
Observacion: {'expresion': '2**20', 'resultado': 1048576}
Accion: obtener_hora_fecha({})
Observacion: {'fecha': '2026-05-09', 'hora': '08:43:31', 'dia_semana': 'Saturday'}
Respuesta Final:
### Información sobre Agentes de IA

1. **AutoGPT**
   - **Resumen**: Es un agente autónomo basado en GPT-4 que descompone y ejecuta tareas complejas de manera independiente.

2. **LangChain Agents**
   - **Resumen**: Es un framework diseñado para construir agentes de modelos de lenguaje (LLM) utilizando herramientas y memoria, lo que permite una mayor funcionalidad.

3. **ReAct (2022)**
   - **Resumen**: Esta técnica entrelaza razonamiento y acción en modelos de lenguaje para abordar tareas de búsqueda de manera más efectiva.

### Cálculo de \(2^{20}\)
El resultado de \(2^{20}\) es **1,048,576**.

### Hora Actual
La fecha y hora actual es **9 de mayo de 2026, 08:43:31** (sábado).
'### Información sobre Agentes de IA\n\n1. **AutoGPT**\n   - **Resumen**: Es un agente autónomo basado en GPT-4 que descompone y ejecuta tareas complejas de manera independiente.\n\n2. **LangChain Agents**\n   - **Resumen**: Es un framework diseñado para construir agentes de modelos de lenguaje (LLM) utilizando herramientas y memoria, lo que permite una mayor funcionalidad.\n\n3. **ReAct (2022)**\n   - **Resumen**: Esta técnica entrelaza razonamiento y acción en modelos de lenguaje para abordar tareas de búsqueda de manera más efectiva.\n\n### Cálculo de \\(2^{20}\\)\nEl resultado de \\(2^{20}\\) es **1,048,576**.\n\n### Hora Actual\nLa fecha y hora actual es **9 de mayo de 2026, 08:43:31** (sábado).'
# Prueba 2: tarea que requiere razonamiento sobre datos de la empresa
agente_con_herramientas(
    'Dame los proyectos activos y calcula el presupuesto total que suman.',
    registro
)
────────────────────────────────────────────────────────────
  TAREA: Dame los proyectos activos y calcula el presupuesto total que suman.
────────────────────────────────────────────────────────────
Accion: leer_base_datos({"tabla": "proyectos"})
Observacion: [{'id': 1, 'nombre': 'AgenteSalud', 'estado': 'activo', 'presupuesto': 150000}, {'id': 2, 'nombre': 'ChatbotLegal', 'estado': 'pausado', 'presupuesto': 80000}, {'id': 3, 'nombre': 'RAG-Docs', 'estado'...
Accion: leer_base_datos({"tabla": "empleados"})
Observacion: [{'id': 1, 'nombre': 'Ana Lopez', 'rol': 'ML Engineer', 'proyecto_id': 1}, {'id': 2, 'nombre': 'Luis Mora', 'rol': 'Investigador', 'proyecto_id': 3}, {'id': 3, 'nombre': 'Sara Ruiz', 'rol': 'Product M...
Accion: calcular({"expresion": "150000 + 60000"})
Observacion: {'expresion': '150000 + 60000', 'resultado': 210000}
Respuesta Final:
Los proyectos activos son:

1. **AgenteSalud**
   - Presupuesto: $150,000
2. **RAG-Docs**
   - Presupuesto: $60,000

El presupuesto total que suman estos proyectos es de **$210,000**.
'Los proyectos activos son:\n\n1. **AgenteSalud**\n   - Presupuesto: $150,000\n2. **RAG-Docs**\n   - Presupuesto: $60,000\n\nEl presupuesto total que suman estos proyectos es de **$210,000**.'

PARTE 4 — ReAct: Razonamiento y Accion Entrelazados#

ReAct (Yao et al., 2022) genera una traza de tres elementos por paso:

\[\tau_t = (\text{Pensamiento}_t,\; \text{Accion}_t,\; \text{Observacion}_t)\]

El razonamiento verbal ancla las acciones con evidencia real, evitando que el modelo invente resultados. La clave: usar stop=["Observacion:"] para que el LLM no se invente las observaciones.

PROMPT_REACT = """
Eres un agente que resuelve tareas paso a paso con razonamiento explicito.

Para cada paso usa EXACTAMENTE este formato:
Pensamiento: <razona sobre lo que sabes y necesitas>
Accion: <nombre_herramienta({"param": "valor"})>

Cuando tengas la respuesta:
Pensamiento: <razona sobre la conclusion>
Respuesta Final: <tu respuesta completa>

Herramientas disponibles:
- buscar_web(query, n_resultados): busca en internet
- calcular(expresion): evalua expresiones matematicas
- ejecutar_python(codigo): ejecuta codigo Python
- obtener_hora_fecha(): retorna fecha y hora
- leer_base_datos(tabla, filtro): consulta base de datos

REGLAS:
- Siempre escribe el Pensamiento ANTES de la Accion.
- Los argumentos de la Accion deben ser JSON valido.
- Nunca inventes resultados; usa las herramientas.
"""


class AgenteReAct:
    """
    Agente ReAct: Razonamiento + Accion en texto natural.

    Diferencia vs. Function Calling nativo:
    - ReAct hace el razonamiento VISIBLE en texto.
    - Permite depurar y auditar el proceso de pensamiento.
    - Funciona con modelos sin soporte de Function Calling.
    - El bucle es implementado por nosotros, no por la API.

    El parametro stop=['Observacion:'] es clave:
    evita que el LLM se invente los resultados de las herramientas
    antes de que nosotros las ejecutemos.
    """

    def __init__(self, registro, modelo=DEFAULT_MODEL, max_pasos=10):
        self.registro  = registro
        self.modelo    = modelo
        self.max_pasos = max_pasos

    def _parsear_accion(self, texto):
        """Extrae nombre_herramienta y {args} del texto generado."""
        match = re.search(r'(\w+)\((.*)\)\s*$', texto.strip(), re.DOTALL)
        if not match:
            return None, {}
        nombre   = match.group(1)
        args_str = match.group(2).strip()
        if not args_str:
            return nombre, {}
        for intento in [args_str, args_str.replace("'", '"')]:
            try:
                return nombre, json.loads(intento)
            except Exception:
                pass
        return nombre, {}

    def resolver(self, tarea):
        separador(f'ReAct — {tarea}')
        mensajes = [
            {'role': 'system', 'content': PROMPT_REACT},
            {'role': 'user',   'content': f'Tarea: {tarea}'}
        ]
        traza = []

        for paso in range(self.max_pasos):
            # stop=['Observacion:']: el LLM se detiene ANTES de inventar la observacion
            respuesta = client.chat.completions.create(
                model=self.modelo, messages=mensajes,
                max_tokens=400, temperature=0.2,
                stop=['Observacion:'],   # <-- clave del patron ReAct
            )
            salida = respuesta.choices[0].message.content.strip()

            # Parsear pensamiento y accion del texto generado
            pensamiento, accion_texto = '', ''
            if 'Pensamiento:' in salida:
                partes = salida.split('Accion:', 1)
                pensamiento = partes[0].replace('Pensamiento:', '').strip()
                accion_texto = partes[1].strip() if len(partes) > 1 else ''

            # Detectar respuesta final
            if 'Respuesta Final:' in salida:
                idx = salida.index('Respuesta Final:')
                pens_txt = salida[:idx].replace('Pensamiento:','').strip()
                resp_txt = salida[idx+len('Respuesta Final:'):].strip()
                if pens_txt: imprimir_pensamiento(pens_txt)
                imprimir_respuesta(resp_txt)
                return resp_txt

            if pensamiento:
                imprimir_pensamiento(pensamiento)

            if accion_texto:
                nombre, args = self._parsear_accion(accion_texto)
                if nombre and nombre in [e['function']['name'] for e in self.registro.esquemas]:
                    imprimir_accion(nombre, args)
                    obs = self.registro.ejecutar(nombre, args)
                    imprimir_observacion(obs)

                    # Construir el siguiente turno: añadir T+A+O al historial
                    contenido_turno = (
                        f'Pensamiento: {pensamiento}\n'
                        f'Accion: {accion_texto}\n'
                        f'Observacion: {json.dumps(obs, ensure_ascii=False, default=str)}'
                    )
                    mensajes.append({'role': 'assistant', 'content': contenido_turno})
                    traza.append({'paso': paso+1, 'pensamiento': pensamiento,
                                  'accion': f'{nombre}({args})', 'observacion': obs})
                    continue

            # Si no hay accion clara, continuar con lo que genero
            mensajes.append({'role': 'assistant', 'content': salida})

        return 'Max pasos alcanzados.'


agente_react = AgenteReAct(registro)

# Demo 1: tarea multi-paso con busqueda
agente_react.resolver('Busca informacion sobre ReAct en IA y luego calcula cuanto es la suma de los primeros 10 numeros primos.')
────────────────────────────────────────────────────────────
  ReAct — Busca informacion sobre ReAct en IA y luego calcula cuanto es la suma de los primeros 10 numeros primos.
────────────────────────────────────────────────────────────
Pensamiento: Primero, necesito buscar información sobre ReAct en inteligencia artificial para entender qué es y cómo se utiliza. Luego, calcularé la suma de los primeros 10 números primos. Comenzaré con la búsqueda de información.  
Accion: buscar_web("ReAct en inteligencia artificial", 5)  

 He realizado la búsqueda sobre ReAct en IA. Ahora, procederé a calcular la suma de los primeros 10 números primos. Los primeros 10 números primos son 2, 3, 5, 7, 11, 13, 17, 19, 23 y 29.  
Accion: calcular("2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29")  

 He calculado la suma de los primeros 10 números primos y ahora tengo tanto la información sobre ReAct en IA como el resultado de la suma.
Respuesta Final:
La información sobre ReAct en IA se refiere a un enfoque que combina razonamiento y acción en sistemas de inteligencia artificial. La suma de los primeros 10 números primos es 129.
'La información sobre ReAct en IA se refiere a un enfoque que combina razonamiento y acción en sistemas de inteligencia artificial. La suma de los primeros 10 números primos es 129.'
# Demo 2: tarea que combina BD + calculo
agente_react.resolver('Consulta los proyectos activos, suma sus presupuestos y dime cuanto es el 15% de ese total.')
────────────────────────────────────────────────────────────
  ReAct — Consulta los proyectos activos, suma sus presupuestos y dime cuanto es el 15% de ese total.
────────────────────────────────────────────────────────────
Pensamiento: Necesito consultar la base de datos para obtener los proyectos activos y sus presupuestos. Luego, sumaré esos presupuestos y calcularé el 15% del total. Primero, realizaré la consulta a la base de datos.
Accion: leer_base_datos({})
Observacion: Error al ejecutar leer_base_datos: leer_base_datos() missing 1 required positional argument: 'tabla'
Pensamiento: Parece que he cometido un error en la llamada a la función. Debo asegurarme de que la tabla y el filtro estén correctamente especificados. Procederé a consultar la base de datos de nuevo para obtener los proyectos activos y sus presupuestos.
Accion: leer_base_datos({})
Observacion: Error al ejecutar leer_base_datos: leer_base_datos() missing 1 required positional argument: 'tabla'
Pensamiento: Me doy cuenta de que no tengo acceso a la base de datos para consultar los proyectos activos. Por lo tanto, necesito buscar información sobre proyectos activos en la web. Esto me permitirá obtener datos sobre los presupuestos de los proyectos.
Accion: buscar_web({})
Observacion: Error al ejecutar buscar_web: buscar_web() missing 1 required positional argument: 'query'
Pensamiento: He cometido un error al intentar buscar en la web. Necesito formular correctamente la consulta para obtener información sobre proyectos activos y sus presupuestos. Procederé a realizar la búsqueda de nuevo.
Accion: buscar_web({})
Observacion: Error al ejecutar buscar_web: buscar_web() missing 1 required positional argument: 'query'
Pensamiento: Me he dado cuenta de que no puedo buscar en la web ni acceder a la base de datos. Necesito una forma de calcular el 15% de un total que no tengo. Por lo tanto, debo asumir que no tengo acceso a la información necesaria para completar la tarea. Sin embargo, puedo mostrarte cómo se haría si tuviera los datos.
Pensamiento: No tengo acceso a los datos necesarios para realizar la tarea, pero puedo explicar cómo se haría el cálculo si tuviera los presupuestos. Primero, sumaría todos los presupuestos de los proyectos activos y luego calcularía el 15% de esa suma. Sin embargo, no puedo proporcionar un resultado numérico sin los datos.
Respuesta Final:
No puedo completar la tarea debido a la falta de acceso a la base de datos o información en la web sobre proyectos activos y sus presupuestos.
'No puedo completar la tarea debido a la falta de acceso a la base de datos o información en la web sobre proyectos activos y sus presupuestos.'

PARTE 5 — Reflexion y Autocorreccion#

Reflexion (Shinn et al., 2023): el agente analiza sus errores pasados para mejorar intentos futuros.

\[\mathcal{R}_t = \text{LLM}_{reflexion}(\tau_{1:t}, r_t)\]

donde \(r_t\) es la señal de exito/fracaso. Las reflexiones se incluyen en el contexto del siguiente intento, mejorando \(P(\text{exito})\).

class AgenteReflexion:
    """
    Agente con capacidad de reflexion sobre sus errores.

    Ciclo de vida:
    1. Intento: el agente intenta resolver la tarea.
    2. Evaluacion: se verifica si la solucion es correcta.
    3. Reflexion: si falla, el agente analiza por que fallo.
    4. Retry: el agente intenta de nuevo con la reflexion como contexto.

    La memoria de reflexiones $R$ crece con cada intento fallido,
    acumulando las lecciones aprendidas para el siguiente intento.
    Las reflexiones se añaden al system prompt de los intentos siguientes.
    """

    PROMPT_REFLEXION = """
    Eres un critico que evalua respuestas de un agente de IA.
    Dado el historial del intento y el resultado, identifica:
    1. Que salio mal o fue suboptimo.
    2. Que deberia hacer diferente el agente.
    3. Que informacion le falta.
    Se conciso (max 3 oraciones). Solo habla de los errores, no de los aciertos.
    """

    def __init__(self, registro=None, modelo=DEFAULT_MODEL, max_intentos=3):
        self.registro     = registro
        self.modelo       = modelo
        self.max_intentos = max_intentos
        self.reflexiones  = []   # memoria de reflexiones acumuladas

    def _intentar(self, tarea):
        """Un intento de resolver la tarea con las reflexiones actuales."""
        # Construir el contexto con reflexiones previas
        contexto_reflexiones = ''
        if self.reflexiones:
            contexto_reflexiones = (
                '\n\nREFLEXIONES DE INTENTOS ANTERIORES (aprende de estos errores):\n' +
                '\n'.join(f'- {r}' for r in self.reflexiones)
            )

        mensajes = [
            {'role': 'system', 'content':
             'Eres un agente de resolucion de problemas preciso. '
             'Da respuestas numericas exactas cuando se piden.' + contexto_reflexiones},
            {'role': 'user', 'content': tarea}
        ]

        if self.registro:
            respuesta = client.chat.completions.create(
                model=self.modelo, messages=mensajes,
                tools=self.registro.esquemas, tool_choice='auto', max_tokens=500,
            )
        else:
            respuesta = client.chat.completions.create(
                model=self.modelo, messages=mensajes, max_tokens=500,
            )

        # Ejecutar herramientas si las hay
        msg = respuesta.choices[0].message
        if self.registro and respuesta.choices[0].finish_reason == 'tool_calls':
            mensajes.append(msg)
            for llamada in msg.tool_calls:
                nombre = llamada.function.name
                args   = json.loads(llamada.function.arguments)
                resultado = self.registro.ejecutar(nombre, args)
                mensajes.append({'role':'tool','tool_call_id':llamada.id,
                                 'content':json.dumps(resultado,default=str)})
            respuesta2 = client.chat.completions.create(
                model=self.modelo, messages=mensajes, max_tokens=300)
            return respuesta2.choices[0].message.content, mensajes
        return msg.content, mensajes

    def _reflexionar(self, tarea, respuesta, historial):
        """El agente critica su propio intento."""
        critica = client.chat.completions.create(
            model=self.modelo,
            messages=[
                {'role': 'system', 'content': self.PROMPT_REFLEXION},
                {'role': 'user', 'content':
                 f'Tarea: {tarea}\nRespuesta del agente: {respuesta}\n'
                 f'Resultado: INCORRECTO o INCOMPLETO.\n'
                 f'Que deberia hacer diferente?'}
            ],
            max_tokens=150, temperature=0.3,
        )
        return critica.choices[0].message.content

    def _evaluar(self, respuesta, criterio):
        """Evaluacion simple: verifica si la respuesta cumple el criterio."""
        # En produccion: usar un evaluador mas robusto (otro LLM, tests unitarios, etc.)
        eval_resp = client.chat.completions.create(
            model=self.modelo,
            messages=[
                {'role': 'system', 'content': 'Responde solo SI o NO.'},
                {'role': 'user', 'content':
                 f'La respuesta \'{respuesta}\' cumple este criterio: {criterio}?'}
            ],
            max_tokens=5, temperature=0.0,
        )
        return 'si' in eval_resp.choices[0].message.content.lower()

    def resolver(self, tarea, criterio_exito=None):
        """
        Resuelve la tarea con hasta max_intentos, reflexionando entre intentos.
        criterio_exito: descripcion de que constituye una respuesta correcta.
        """
        separador(f'Reflexion — {tarea}')
        self.reflexiones = []

        for intento in range(1, self.max_intentos + 1):
            print(f'\n{BOLD}[Intento {intento}/{self.max_intentos}]{RESET}')
            if self.reflexiones:
                print(f'  Reflexiones acumuladas: {len(self.reflexiones)}')

            respuesta, historial = self._intentar(tarea)
            print(f'  Respuesta: {respuesta}')

            # Evaluar si la respuesta es correcta
            if criterio_exito:
                es_correcta = self._evaluar(respuesta, criterio_exito)
                if es_correcta:
                    imprimir_respuesta(f'Correcto en el intento {intento}: {respuesta}')
                    return respuesta, intento
                else:
                    # Reflexionar sobre el error para el siguiente intento
                    reflexion = self._reflexionar(tarea, respuesta, historial)
                    print(f'  {AMARILLO}Reflexion: {reflexion}{RESET}')
                    self.reflexiones.append(reflexion)
            else:
                imprimir_respuesta(respuesta)
                return respuesta, intento

        print(f'{ROJO}No se alcanzo la respuesta correcta en {self.max_intentos} intentos.{RESET}')
        return respuesta, self.max_intentos


# Demo: tarea que puede requerir reflexion
agente_ref = AgenteReflexion(registro=registro, max_intentos=3)

resultado, n_intentos = agente_ref.resolver(
    'Ejecuta este codigo Python: [x**2 for x in range(1,6)] y dime la suma de todos los valores.',
    criterio_exito='contiene el numero 55 (1+4+9+16+25=55)'
)
print(f'\nResuelto en {n_intentos} intento(s).')
────────────────────────────────────────────────────────────
  Reflexion — Ejecuta este codigo Python: [x**2 for x in range(1,6)] y dime la suma de todos los valores.
────────────────────────────────────────────────────────────

[Intento 1/3]
  Respuesta: El código Python `[x**2 for x in range(1,6)]` genera una lista de los cuadrados de los números del 1 al 5, que son: `[1, 4, 9, 16, 25]`. 

La suma de estos valores es:

\[ 1 + 4 + 9 + 16 + 25 = 55 \]

Por lo tanto, la suma de todos los valores es **55**.
Respuesta Final:
Correcto en el intento 1: El código Python `[x**2 for x in range(1,6)]` genera una lista de los cuadrados de los números del 1 al 5, que son: `[1, 4, 9, 16, 25]`. 

La suma de estos valores es:

\[ 1 + 4 + 9 + 16 + 25 = 55 \]

Por lo tanto, la suma de todos los valores es **55**.

Resuelto en 1 intento(s).

PARTE 6 — Memoria de Agentes#

Los agentes tienen diferentes tipos de memoria:

Tipo

Implementacion

Limite

Corto plazo

Ventana de contexto

\(L_{max}\) tokens

Largo plazo

Base de datos vectorial

Sin limite practico

Episodica

Trazas indexadas por tiempo

Recuperacion por similaridad

La recuperacion semantica usa similitud coseno: \(\text{sim}(q, m) = \frac{q^\top m}{\|q\|\|m\|}\)

class MemoriaAgente:
    """
    Sistema de memoria de dos niveles para agentes LLM.

    CORTO PLAZO: ventana de contexto del LLM.
    - Limite: L_max tokens (tipicamente 128K en gpt-4o-mini).
    - Estrategia cuando se llena: comprimir con resumen o truncar.

    LARGO PLAZO: almacen vectorial con recuperacion semantica.
    - Almacena memorias como embeddings de texto.
    - Recupera las mas relevantes por similitud coseno.
    - Indexadas por tiempo para recuperacion cronologica.
    - Cada memoria tiene importancia (1-10) y recencia.

    Recuperacion ponderada (Generative Agents, Park et al. 2023):
    score = alpha_r * recencia + alpha_i * importancia + alpha_s * relevancia
    """

    def __init__(self, capacidad_corto=10, modelo_embed='text-embedding-3-small',
                 alpha_r=0.5, alpha_i=0.3, alpha_s=0.2):
        # Corto plazo: lista FIFO de mensajes recientes
        self.corto_plazo     = []
        self.capacidad_corto = capacidad_corto

        # Largo plazo: lista de diccionarios con embedding y metadatos
        self.largo_plazo     = []
        self.modelo_embed    = modelo_embed

        # Pesos para la recuperacion ponderada
        self.alpha_r, self.alpha_i, self.alpha_s = alpha_r, alpha_i, alpha_s

    def _embed(self, texto):
        """Convierte texto a vector de embeddings usando la API de OpenAI."""
        texto = texto.replace('\n', ' ')
        resp  = client.embeddings.create(input=texto, model=self.modelo_embed)
        return np.array(resp.data[0].embedding)

    def _similitud_coseno(self, a, b):
        """sim(a,b) = a·b / (||a|| * ||b||). Rango [-1,1], tipicamente [0,1] para embeddings."""
        return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8))

    def recordar_corto(self, rol, contenido):
        """Añadir mensaje a la memoria de corto plazo. FIFO si supera la capacidad."""
        self.corto_plazo.append({'role': rol, 'content': contenido, 'timestamp': time.time()})
        if len(self.corto_plazo) > self.capacidad_corto:
            self.corto_plazo.pop(0)   # eliminar el mas antiguo

    def guardar_largo(self, contenido, importancia=5):
        """
        Guardar una memoria en el almacen de largo plazo.
        importancia: 1 (trivial) a 10 (critica)
        El embedding permite recuperacion semantica posterior.
        """
        embedding = self._embed(contenido)
        self.largo_plazo.append({
            'contenido'   : contenido,
            'embedding'   : embedding,
            'importancia' : importancia / 10.0,       # normalizar a [0,1]
            'timestamp'   : time.time(),
            'ultimo_acceso': time.time(),
        })

    def recuperar(self, query, k=3, decay=0.99):
        """
        Recupera las k memorias mas relevantes para el query.

        Ponderacion de tres factores:
        - Recencia: exp(-lambda * delta_t), memorias recientes tienen mayor peso.
        - Importancia: asignada al guardar la memoria.
        - Relevancia: similitud coseno con el query.

        El parametro decay controla que tan rapido decae la recencia.
        decay=0.99: decae lentamente. decay=0.5: decae rapidamente.
        """
        if not self.largo_plazo:
            return []
        q_emb  = self._embed(query)
        ahora  = time.time()
        scored = []

        for mem in self.largo_plazo:
            dt = (ahora - mem['timestamp']) / 3600   # en horas
            # Recencia: decaimiento exponencial desde el ultimo guardado
            recencia    = decay ** dt
            importancia = mem['importancia']
            relevancia  = self._similitud_coseno(q_emb, mem['embedding'])
            # Score ponderado
            score = (self.alpha_r * recencia +
                     self.alpha_i * importancia +
                     self.alpha_s * relevancia)
            scored.append((score, mem['contenido']))

        scored.sort(key=lambda x: x[0], reverse=True)
        return [contenido for _, contenido in scored[:k]]

    def comprimir_corto_plazo(self):
        """
        Comprime la memoria de corto plazo en un resumen.
        Util cuando la ventana de contexto esta casi llena.
        Se guarda el resumen en largo plazo y se limpia el corto plazo.
        """
        if len(self.corto_plazo) < 3:
            return
        texto = '\n'.join(f"[{m['role']}]: {m['content']}" for m in self.corto_plazo)
        resumen_resp = client.chat.completions.create(
            model=DEFAULT_MODEL,
            messages=[
                {'role':'system','content':'Resume esta conversacion en 2-3 oraciones, preservando los hechos clave.'},
                {'role':'user',  'content':texto}
            ], max_tokens=150,
        )
        resumen = resumen_resp.choices[0].message.content
        self.guardar_largo(f'[Resumen de conversacion]: {resumen}', importancia=7)
        self.corto_plazo = []   # limpiar corto plazo
        print(f'{AZUL}[Memoria] Corto plazo comprimido. Resumen guardado en largo plazo.{RESET}')
        return resumen


class AgenteConMemoria:
    """
    Agente LLM con sistema de memoria de dos niveles.

    En cada interaccion:
    1. Guardar la percepcion en corto plazo.
    2. Recuperar memorias relevantes del largo plazo.
    3. Construir el contexto: [system + memorias_relevantes + corto_plazo].
    4. Generar la respuesta.
    5. Guardar la respuesta en corto plazo.
    """

    def __init__(self, nombre, system_prompt, modelo=DEFAULT_MODEL):
        self.nombre   = nombre
        self.sistema  = system_prompt
        self.modelo   = modelo
        self.memoria  = MemoriaAgente()

    def interactuar(self, mensaje, guardar_en_largo=False, importancia=5):
        # Guardar en corto plazo
        self.memoria.recordar_corto('user', mensaje)

        # Recuperar memorias relevantes del largo plazo
        memorias = self.memoria.recuperar(mensaje, k=3)

        # Construir el contexto completo
        contexto_memoria = ''
        if memorias:
            contexto_memoria = (
                '\n\nMEMORIAS RELEVANTES (informacion de conversaciones anteriores):\n' +
                '\n'.join(f'- {m}' for m in memorias)
            )

        mensajes = [
            {'role': 'system', 'content': self.sistema + contexto_memoria},
            # Añadir el corto plazo (conversacion reciente)
            *[{'role': m['role'], 'content': m['content']} for m in self.memoria.corto_plazo],
        ]

        respuesta = client.chat.completions.create(
            model=self.modelo, messages=mensajes, max_tokens=400, temperature=0.7,
        )
        resp_texto = respuesta.choices[0].message.content
        self.memoria.recordar_corto('assistant', resp_texto)

        if guardar_en_largo:
            resumen_interaccion = f'Usuario pregunto: {mensaje}. Respuesta: {resp_texto[:100]}'
            self.memoria.guardar_largo(resumen_interaccion, importancia=importancia)

        return resp_texto


# Demo del sistema de memoria
asistente = AgenteConMemoria(
    nombre='AsistentePersonal',
    system_prompt='Eres un asistente personal que recuerda las preferencias y contexto del usuario.'
)

separador('DEMO — Memoria de Agentes')

# Poblar la memoria de largo plazo con informacion del usuario
asistente.memoria.guardar_largo('El usuario trabaja en ciencia de datos y usa Python principalmente.', importancia=8)
asistente.memoria.guardar_largo('El usuario prefiere respuestas concisas y con ejemplos de codigo.', importancia=9)
asistente.memoria.guardar_largo('El proyecto actual del usuario es un sistema de recomendacion con LLMs.', importancia=7)
asistente.memoria.guardar_largo('El usuario tiene experiencia en TensorFlow y PyTorch.', importancia=6)
print('Memoria de largo plazo inicializada con 4 recuerdos.\n')

r1 = asistente.interactuar('Que lenguaje deberia usar para mi proyecto?', guardar_en_largo=True)
print(f'[T1] {r1}\n')

r2 = asistente.interactuar('Como puedo optimizar mi modelo de recomendacion?')
print(f'[T2] {r2}\n')

r3 = asistente.interactuar('Dame un ejemplo rapido de como estructurar el codigo.')
print(f'[T3] {r3}\n')

print(f'Memorias en largo plazo: {len(asistente.memoria.largo_plazo)}')
print(f'Mensajes en corto plazo: {len(asistente.memoria.corto_plazo)}')
────────────────────────────────────────────────────────────
  DEMO — Memoria de Agentes
────────────────────────────────────────────────────────────
Memoria de largo plazo inicializada con 4 recuerdos.

[T1] Para tu proyecto de sistema de recomendación con LLMs, Python es la mejor opción. Tiene bibliotecas robustas para procesamiento de lenguaje natural (como `transformers` de Hugging Face) y ciencia de datos (como `pandas` y `numpy`). Aquí tienes un ejemplo básico de cómo cargar un modelo LLM en Python:

```python
from transformers import pipeline

# Cargar modelo de lenguaje
model = pipeline('text-generation', model='gpt-2')

# Generar texto
output = model("Me gustaría recibir recomendaciones sobre", max_length=50)
print(output[0]['generated_text'])
```

Si necesitas más ayuda o ejemplos, ¡hazmelo saber!

[T2] Para optimizar tu modelo de recomendación, considera las siguientes estrategias:

1. **Ajuste de Hiperparámetros**: Usa técnicas como Grid Search o Random Search para encontrar la mejor configuración de hiperparámetros.

   ```python
   from sklearn.model_selection import GridSearchCV
   from sklearn.ensemble import RandomForestClassifier

   param_grid = {'n_estimators': [10, 50, 100], 'max_depth': [None, 10, 20]}
   grid_search = GridSearchCV(RandomForestClassifier(), param_grid, cv=5)
   grid_search.fit(X_train, y_train)
   ```

2. **Feature Engineering**: Crea nuevas características que puedan capturar mejor la información relevante para las recomendaciones.

3. **Regularización**: Aplica técnicas de regularización (L1, L2) para evitar el sobreajuste.

4. **Modelos de Ensamble**: Combina múltiples modelos para mejorar la precisión.

   ```python
   from sklearn.ensemble import VotingClassifier

   model1 = RandomForestClassifier()
   model2 = GradientBoostingClassifier()
   ensemble_model = VotingClassifier(estimators=[('rf', model1), ('gb', model2)], voting='soft')
   ensemble_model.fit(X_train, y_train)
   ```

5. **Evaluación Continua**: Usa métricas adecuadas (como precisión, recall, F1-score) y valida tu modelo regularmente.

6. **Uso de LLMs**: Si estás usando LLMs, considera fine-tuning para adaptar el modelo a tu dominio específico.

Si necesitas más detalles sobre alguna de estas estrategias, ¡avísame!

[T3] Claro, aquí tienes un ejemplo básico de cómo estructurar el código para un sistema de recomendación utilizando un modelo de lenguaje y algunas de las técnicas mencionadas. Este ejemplo asume que tienes un conjunto de datos y quieres hacer recomendaciones basadas en el contenido.

```python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
from transformers import pipeline

# Cargar datos
data = pd.read_csv('datos_recomendacion.csv')

# Preprocesamiento
# Aquí puedes hacer limpieza, normalización, etc.
X = data.drop('etiqueta', axis=1)  # Características
y = data['etiqueta']  # Etiquetas

# Dividir en conjunto de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Entrenar modelo
model = RandomForestClassifier(n_estimators=100)
model.fit(X_train, y_train)

# Hacer predicciones
y_pred = model.predict(X_test)

# Evaluar modelo
print(classification_report(y_test, y_pred))

# Usar LLM para recomendaciones
llm = pipeline('text-generation', model='gpt-2')
input_text = "Me gustaría recibir recomendaciones sobre"
recommendation = llm(input_text, max_length=50)
print(recommendation[0]['generated_text'])
```

Este código cubre desde la carga y preprocesamiento de datos hasta el entrenamiento de un modelo y el uso de un LLM para generar recomendaciones. Puedes ajustar cada parte según tu proyecto específico. Si necesitas más detalles o alguna modificación, ¡dímelo!

Memorias en largo plazo: 5
Mensajes en corto plazo: 6

PARTE 7 — Planificacion: CoT y Descomposicion de Tareas#

Chain-of-Thought (CoT): el modelo genera pasos intermedios explicitamente, aumentando el computo efectivo por token.

Descomposicion jerarquica: tarea compleja \(T \to (T_1, T_2, \ldots, T_k)\), donde cada subtarea se resuelve independientemente.

def chain_of_thought(tarea, modelo=DEFAULT_MODEL):
    """
    Chain-of-Thought: el agente razona paso a paso antes de responder.

    'Piensa paso a paso' activa el modo CoT en los LLMs modernos.
    Mejora drasticamente tareas de razonamiento multi-paso, matematica y logica.
    El mecanismo: genera tokens intermedios de razonamiento antes de la respuesta,
    efectivamente aumentando el computo disponible de O(d_ff) a O(T_cot * d_ff).
    """
    respuesta = client.chat.completions.create(
        model=modelo,
        messages=[
            {'role':'system','content':
             'Razona paso a paso antes de responder. '
             'Muestra tu proceso de pensamiento claramente numerado.'},
            {'role':'user','content':tarea}
        ],
        max_tokens=600, temperature=0.3,
    )
    return respuesta.choices[0].message.content


def descomponer_y_resolver(tarea_compleja, registro, modelo=DEFAULT_MODEL):
    """
    Planificacion jerarquica: descomponer una tarea compleja en subtareas.

    Paso 1 (Planificador): el LLM genera un plan con subtareas numeradas.
    Paso 2 (Ejecutor): cada subtarea se resuelve con herramientas.
    Paso 3 (Sintetizador): el LLM integra los resultados en una respuesta final.

    La separacion de roles (planificar vs. ejecutar vs. sintetizar) reduce
    la carga cognitiva en cada llamada y mejora la calidad de cada parte.
    """
    separador(f'PLANIFICACION — {tarea_compleja}')

    # PASO 1: Planificar
    print(f'{AZUL}[Planificador]{RESET} Descomponiendo la tarea...')
    plan_resp = client.chat.completions.create(
        model=modelo,
        messages=[
            {'role':'system','content':
             'Eres un planificador. Descompone la tarea en 3-5 subtareas concretas. '
             'Responde SOLO con una lista numerada. Sin explicaciones adicionales.'},
            {'role':'user','content':f'Tarea: {tarea_compleja}'}
        ],
        max_tokens=300, temperature=0.3,
    )
    plan_texto = plan_resp.choices[0].message.content
    print(f'{plan_texto}\n')

    # Parsear el plan en subtareas
    subtareas = []
    for linea in plan_texto.split('\n'):
        linea = linea.strip()
        if linea and (linea[0].isdigit() or linea.startswith('-')):
            subtarea = re.sub(r'^[\d\.\-\)\s]+', '', linea).strip()
            if subtarea:
                subtareas.append(subtarea)

    # PASO 2: Ejecutar cada subtarea
    resultados = {}
    for i, subtarea in enumerate(subtareas, 1):
        print(f'{AMARILLO}[Subtarea {i}/{len(subtareas)}]{RESET} {subtarea}')
        resultado = agente_con_herramientas(subtarea, registro, verbose=False)
        resultados[f'Subtarea {i}'] = resultado
        print(f'  -> {resultado[:150]}...\n' if len(resultado)>150 else f'  -> {resultado}\n')

    # PASO 3: Sintetizar
    print(f'{VERDE}[Sintetizador]{RESET} Integrando resultados...')
    sintesis_resp = client.chat.completions.create(
        model=modelo,
        messages=[
            {'role':'system','content':
             'Integra los resultados de las subtareas en una respuesta coherente y completa.'},
            {'role':'user','content':
             f'Tarea original: {tarea_compleja}\n\n'
             f'Resultados de subtareas:\n' +
             '\n'.join(f'{k}: {v}' for k,v in resultados.items())}
        ],
        max_tokens=500,
    )
    respuesta_final = sintesis_resp.choices[0].message.content
    imprimir_respuesta(respuesta_final)
    return respuesta_final


# Demo CoT
separador('Chain-of-Thought')
razonamiento = chain_of_thought(
    'Un tren sale de A a 80 km/h. Otro sale de B (320 km de A) a 120 km/h en direccion contraria. '
    'Cuanto tardan en encontrarse y a que distancia de A ocurre el encuentro?'
)
print(razonamiento)
────────────────────────────────────────────────────────────
  Chain-of-Thought
────────────────────────────────────────────────────────────
Para resolver el problema, sigamos estos pasos:

1. **Identificar las velocidades y distancias**:
   - Tren 1 (desde A): velocidad = 80 km/h
   - Tren 2 (desde B): velocidad = 120 km/h
   - Distancia entre A y B = 320 km

2. **Determinar la velocidad relativa**:
   - Como los trenes se mueven en direcciones opuestas, sumamos sus velocidades para encontrar la velocidad relativa.
   - Velocidad relativa = 80 km/h + 120 km/h = 200 km/h

3. **Calcular el tiempo hasta el encuentro**:
   - Usamos la fórmula: tiempo = distancia / velocidad.
   - La distancia entre los trenes es de 320 km y la velocidad relativa es de 200 km/h.
   - Tiempo = 320 km / 200 km/h = 1.6 horas.

4. **Calcular la distancia recorrida por el tren 1 hasta el encuentro**:
   - Usamos la fórmula: distancia = velocidad × tiempo.
   - Distancia recorrida por el tren 1 = 80 km/h × 1.6 h = 128 km.

5. **Conclusiones**:
   - Los trenes tardan 1.6 horas en encontrarse.
   - El encuentro ocurre a 128 km de A.

Por lo tanto, la respuesta es que tardan 1.6 horas en encontrarse y el encuentro ocurre a 128 km de A.
# Demo descomposicion jerarquica
descomponer_y_resolver(
    'Analiza la situacion de los proyectos de la empresa: que proyectos hay activos, '
    'cuanto suman sus presupuestos, y da una recomendacion sobre si el presupuesto es suficiente '
    'para un equipo promedio de 5 personas con salario de 8000 USD/mes cada una.',
    registro
)
────────────────────────────────────────────────────────────
  PLANIFICACION — Analiza la situacion de los proyectos de la empresa: que proyectos hay activos, cuanto suman sus presupuestos, y da una recomendacion sobre si el presupuesto es suficiente para un equipo promedio de 5 personas con salario de 8000 USD/mes cada una.
────────────────────────────────────────────────────────────
[Planificador] Descomponiendo la tarea...
1. Identificar y listar todos los proyectos activos de la empresa.
2. Recopilar información sobre los presupuestos asignados a cada proyecto.
3. Calcular la suma total de los presupuestos de los proyectos activos.
4. Determinar el costo total del equipo promedio de 5 personas con un salario de 8000 USD/mes cada una.
5. Comparar el total de los presupuestos con el costo del equipo y elaborar una recomendación.

[Subtarea 1/5] Identificar y listar todos los proyectos activos de la empresa.
  -> Los proyectos activos de la empresa son los siguientes:

1. **Nombre:** AgenteSalud
   - **Estado:** Activo
   - **Presupuesto:** $150,000

2. **Nombr...

[Subtarea 2/5] Recopilar información sobre los presupuestos asignados a cada proyecto.
  -> Aquí tienes la información sobre los presupuestos asignados a cada proyecto:

1. **AgenteSalud**
   - Estado: Activo
   - Presupuesto: $150,000

2. **...

[Subtarea 3/5] Calcular la suma total de los presupuestos de los proyectos activos.
  -> La suma total de los presupuestos de los proyectos activos es de **210,000**.

[Subtarea 4/5] Determinar el costo total del equipo promedio de 5 personas con un salario de 8000 USD/mes cada una.
  -> El costo total del equipo promedio de 5 personas, con un salario de 8000 USD/mes cada una, es de 40,000 USD al mes.

[Subtarea 5/5] Comparar el total de los presupuestos con el costo del equipo y elaborar una recomendación.
  -> Los datos de los proyectos indican que el total de los presupuestos asignados es de **$290,000**. 

En cuanto al costo del equipo tecnológico para pro...

[Sintetizador] Integrando resultados...
Respuesta Final:
### Análisis de la Situación de los Proyectos de la Empresa

**Proyectos Activos:**

Actualmente, la empresa tiene dos proyectos activos:

1. **AgenteSalud**
   - **Presupuesto:** $150,000
   - **Estado:** Activo

2. **RAG-Docs**
   - **Presupuesto:** $60,000
   - **Estado:** Activo

**Suma Total de Presupuestos:**

La suma total de los presupuestos de los proyectos activos es de **$210,000**. Es importante mencionar que el presupuesto total asignado a los proyectos en general es de **$290,000**, incluyendo tanto proyectos activos como pausados.

**Costos del Equipo:**

Para un equipo promedio de 5 personas, con un salario de $8,000/USD al mes cada uno, el costo mensual total asciende a **$40,000**. Esto implica que para un período de 6 meses, el costo total del equipo sería de **$240,000**.

### Recomendaciones:

1. **Evaluación de Costos**: Es fundamental realizar un desglose detallado de los costos necesarios para la ejecución de cada proyecto. Esto incluye no solo los salarios del equipo, sino también otros gastos operativos, materiales y tecnologías requeridas.

2. **Ajuste Presupuestario**: Si el costo total del equipo y los otros gastos previstos superan el presupuesto de $210,000 para los proyectos activos, se debería considerar solicitar un ajuste al presupuesto de la empresa o buscar formas más económicas de implementar los requerimientos necesarios.

3. **Priorizar Proyectos**: Dado que actualmente solo hay dos proyectos activos, la empresa debería enfocarse en asegurar que estos proyectos tengan los recursos necesarios para su completa ejecución, en lugar de dispersar esfuerzos en proyectos que están pausados.

4. **Investigación Adicional**: Se sugiere realizar una investigación más profunda sobre los costos específicos que pueden surgir durante la ejecución de estos proyectos. Esto ayudará a tomar decisiones más informadas sobre el uso de recursos y la gestión del presupuesto.

### Conclusión

A través de este análisis, se ha identificado que los presupuestos destinados a los proyectos activos son insuficientes para cubrir los costos de un equipo promedio durante un período prolongado. Por lo tanto, es crucial atender proactivamente estas recomendaciones para asegurar la viabilidad y el éxito de los proyectos en curso.
'### Análisis de la Situación de los Proyectos de la Empresa\n\n**Proyectos Activos:**\n\nActualmente, la empresa tiene dos proyectos activos:\n\n1. **AgenteSalud**\n   - **Presupuesto:** $150,000\n   - **Estado:** Activo\n\n2. **RAG-Docs**\n   - **Presupuesto:** $60,000\n   - **Estado:** Activo\n\n**Suma Total de Presupuestos:**\n\nLa suma total de los presupuestos de los proyectos activos es de **$210,000**. Es importante mencionar que el presupuesto total asignado a los proyectos en general es de **$290,000**, incluyendo tanto proyectos activos como pausados.\n\n**Costos del Equipo:**\n\nPara un equipo promedio de 5 personas, con un salario de $8,000/USD al mes cada uno, el costo mensual total asciende a **$40,000**. Esto implica que para un período de 6 meses, el costo total del equipo sería de **$240,000**.\n\n### Recomendaciones:\n\n1. **Evaluación de Costos**: Es fundamental realizar un desglose detallado de los costos necesarios para la ejecución de cada proyecto. Esto incluye no solo los salarios del equipo, sino también otros gastos operativos, materiales y tecnologías requeridas.\n\n2. **Ajuste Presupuestario**: Si el costo total del equipo y los otros gastos previstos superan el presupuesto de $210,000 para los proyectos activos, se debería considerar solicitar un ajuste al presupuesto de la empresa o buscar formas más económicas de implementar los requerimientos necesarios.\n\n3. **Priorizar Proyectos**: Dado que actualmente solo hay dos proyectos activos, la empresa debería enfocarse en asegurar que estos proyectos tengan los recursos necesarios para su completa ejecución, en lugar de dispersar esfuerzos en proyectos que están pausados.\n\n4. **Investigación Adicional**: Se sugiere realizar una investigación más profunda sobre los costos específicos que pueden surgir durante la ejecución de estos proyectos. Esto ayudará a tomar decisiones más informadas sobre el uso de recursos y la gestión del presupuesto.\n\n### Conclusión\n\nA través de este análisis, se ha identificado que los presupuestos destinados a los proyectos activos son insuficientes para cubrir los costos de un equipo promedio durante un período prolongado. Por lo tanto, es crucial atender proactivamente estas recomendaciones para asegurar la viabilidad y el éxito de los proyectos en curso.'

PARTE 8 — Sistemas Multi-Agente#

Los sistemas multi-agente permiten especializacion, paralelizacion y verificacion cruzada:

\[P(\text{exito})_{multi} \geq 1 - \prod_{i=1}^n (1 - P(\text{exito})_i)\]

Patron orquestador-especialistas: un agente coordinador delega subtareas a agentes expertos.

class AgentesEspecialistas:
    """
    Conjunto de agentes especializados con roles distintos.

    Cada especialista tiene:
    - Un system prompt especifico para su dominio.
    - Opcionalmente, herramientas propias.
    - Una funcion 'ejecutar' que realiza su tarea.
    """

    def __init__(self, registro, modelo=DEFAULT_MODEL):
        self.registro = registro
        self.modelo   = modelo

        # Definicion de especialistas con sus system prompts
        self.especialistas = {
            'investigador': {
                'nombre': 'Investigador',
                'color': AZUL,
                'prompt': 'Eres un investigador experto. Buscas informacion factual y la presentas con precision. '
                          'Siempre buscas en la web antes de responder sobre temas actuales.',
                'usa_herramientas': True,
            },
            'analista': {
                'nombre': 'Analista de Datos',
                'color': VERDE,
                'prompt': 'Eres un analista de datos. Calculas, ejecutas codigo Python para analisis '
                          'y presentas resultados con numeros precisos y graficos si aplica.',
                'usa_herramientas': True,
            },
            'escritor': {
                'nombre': 'Escritor',
                'color': MAGENTA,
                'prompt': 'Eres un escritor experto. Redactas informes claros, bien estructurados '
                          'y con el tono adecuado para la audiencia. '
                          'Integras la informacion que te dan sin inventar datos.',
                'usa_herramientas': False,
            },
            'critico': {
                'nombre': 'Critico',
                'color': ROJO,
                'prompt': 'Eres un critico constructivo. Evaluas respuestas y borradores '
                          'identificando errores, vacios logicos y areas de mejora. '
                          'Eres directo pero justo.',
                'usa_herramientas': False,
            },
        }

    def ejecutar_especialista(self, rol, tarea, contexto=''):
        """Ejecuta un especialista especifico sobre una tarea."""
        if rol not in self.especialistas:
            return f'Especialista {rol} no encontrado.'

        esp     = self.especialistas[rol]
        color   = esp['color']
        nombre  = esp['nombre']
        print(f'\n{color}{BOLD}[{nombre}]{RESET} Ejecutando: {tarea[:80]}...')

        mensajes = [
            {'role': 'system', 'content': esp['prompt'] +
             (f'\n\nContexto previo:\n{contexto}' if contexto else '')},
            {'role': 'user', 'content': tarea}
        ]

        if esp['usa_herramientas']:
            return agente_con_herramientas(tarea + (f'. Contexto: {contexto}' if contexto else ''),
                                           self.registro, verbose=False)
        else:
            resp = client.chat.completions.create(
                model=self.modelo, messages=mensajes,
                max_tokens=500, temperature=0.7,
            )
            resultado = resp.choices[0].message.content
            print(f'{color}{resultado[:200]}...{RESET}' if len(resultado)>200 else f'{color}{resultado}{RESET}')
            return resultado


class OrquestadorMultiAgente:
    """
    Orquestador que coordina multiples agentes especialistas.

    El orquestador:
    1. Recibe la tarea de alto nivel.
    2. Decide que especialistas necesita y en que orden.
    3. Pasa los resultados de cada especialista al siguiente.
    4. Sintetiza la respuesta final.

    Topologia: ESTRELLA (centralizado)
    El orquestador es el punto central de coordinacion.
    Ventaja: control simple. Desventaja: cuello de botella.
    """

    def __init__(self, registro, modelo=DEFAULT_MODEL):
        self.especialistas_pool = AgentesEspecialistas(registro, modelo)
        self.modelo             = modelo
        self.log_conversaciones = []   # historial de todas las interacciones

    def _planificar(self, tarea):
        """
        El orquestador decide que especialistas usar y en que secuencia.
        Retorna una lista de pasos: [(rol, descripcion_tarea)].
        """
        plan_resp = client.chat.completions.create(
            model=self.modelo,
            messages=[
                {'role': 'system', 'content':
                 'Eres un coordinador de agentes. Decide que especialistas usar para resolver la tarea. '
                 'Especialistas disponibles: investigador, analista, escritor, critico. '
                 'Responde con una lista JSON de pasos: [{"rol": "...", "tarea": "..."}]. '
                 'Solo JSON, sin texto adicional. Maximo 4 pasos.'},
                {'role': 'user', 'content': f'Tarea: {tarea}'}
            ],
            max_tokens=300, temperature=0.2,
        )
        try:
            texto = plan_resp.choices[0].message.content.strip()
            texto = re.sub(r'^```.*?\n', '', texto, flags=re.DOTALL)
            texto = texto.replace('```', '').strip()
            return json.loads(texto)
        except Exception:
            # Si falla el parsing, plan por defecto
            return [
                {'rol': 'investigador', 'tarea': f'Investiga: {tarea}'},
                {'rol': 'escritor',     'tarea': f'Escribe un informe sobre: {tarea}'},
            ]

    def resolver(self, tarea):
        """
        Ejecuta el pipeline multi-agente completo.
        El contexto se pasa encadenado de un especialista al siguiente.
        """
        separador(f'MULTI-AGENTE — {tarea}')

        # Planificacion
        print(f'{BOLD}[Orquestador]{RESET} Planificando pipeline...')
        plan = self._planificar(tarea)
        print(f'Pipeline: {" -> ".join(p["rol"] for p in plan)}\n')

        # Ejecucion secuencial con contexto encadenado
        contexto_acumulado = ''
        resultados = {}

        for paso in plan:
            rol   = paso.get('rol', 'escritor')
            tarea_esp = paso.get('tarea', tarea)
            resultado = self.especialistas_pool.ejecutar_especialista(
                rol, tarea_esp, contexto=contexto_acumulado
            )
            resultados[rol] = resultado
            # El resultado de cada especialista se convierte en contexto para el siguiente
            contexto_acumulado += f'\n[{rol}]: {resultado}\n'
            self.log_conversaciones.append({'rol': rol, 'tarea': tarea_esp, 'resultado': resultado})

        # Sintesis final por el orquestador
        print(f'\n{BOLD}[Orquestador]{RESET} Sintetizando respuesta final...')
        sintesis = client.chat.completions.create(
            model=self.modelo,
            messages=[
                {'role':'system','content':'Sintetiza los aportes de los especialistas en una respuesta final cohesiva.'},
                {'role':'user','content': f'Tarea original: {tarea}\n\nAportes:\n{contexto_acumulado}'}
            ],
            max_tokens=500,
        )
        respuesta_final = sintesis.choices[0].message.content
        imprimir_respuesta(respuesta_final)
        return respuesta_final


# Demo del sistema multi-agente
orquestador = OrquestadorMultiAgente(registro)

resultado_multi = orquestador.resolver(
    'Investiga que son los agentes de IA con ReAct, '
    'analiza los proyectos activos de la empresa y su presupuesto, '
    'y escribe un informe ejecutivo de 3 parrafos recomendando si implementar agentes IA en la empresa.'
)
────────────────────────────────────────────────────────────
  MULTI-AGENTE — Investiga que son los agentes de IA con ReAct, analiza los proyectos activos de la empresa y su presupuesto, y escribe un informe ejecutivo de 3 parrafos recomendando si implementar agentes IA en la empresa.
────────────────────────────────────────────────────────────
[Orquestador] Planificando pipeline...
Pipeline: investigador -> analista -> escritor


[Investigador] Ejecutando: Investigar qué son los agentes de IA con ReAct....

[Analista de Datos] Ejecutando: Analizar los proyectos activos de la empresa y su presupuesto....

[Escritor] Ejecutando: Escribir un informe ejecutivo de 3 párrafos recomendando la implementación de ag...
### Informe Ejecutivo sobre la Implementación de Agentes de IA

En el contexto actual de transformación digital y la creciente necesidad de optimizar procesos, la implementación de agentes de intelige...

[Orquestador] Sintetizando respuesta final...
Respuesta Final:
### Informe Ejecutivo sobre la Implementación de Agentes de IA

La adopción de agentes de inteligencia artificial (IA) en nuestra empresa representa una oportunidad estratégica en un entorno cada vez más digitalizado. Estas herramientas, fundamentadas en tecnologías como AutoGPT y LangChain, permiten la descomposición y ejecución de tareas complejas de manera autónoma, lo que incrementa la eficiencia operativa y mejora la capacidad de respuesta ante diversos desafíos. Actualmente, nuestros proyectos activos, tales como AgenteSalud y RAG-Docs, que ya utilizan técnicas avanzadas de IA, establecen un contexto favorable para la integración de agentes de IA adicionales en nuestra operación.

La metodología ReAct, que combina razonamiento y acción, otorga a los modelos de lenguaje la habilidad de realizar búsquedas y tomar decisiones fundamentadas basadas en su comprensión del contexto. Este avance no solo amplía las capacidades de los agentes de IA, sino que también mejora la calidad de la interacción con los usuarios y la eficacia en la ejecución de tareas. Con un presupuesto total de $210,000 asignado a proyectos que se alinean con estas tecnologías de IA, la inversión en agentes se justifica tanto por el incremento esperado en eficiencia como por la oportunidad que brinda para innovar y diversificar nuestros servicios y productos.

Finalmente, la implementación de agentes de IA no solo posicionará a nuestra empresa a la vanguardia de la tecnología, sino que también reforzará su competitividad en el mercado. Gracias a estos avances, podremos optimizar la toma de decisiones, mejorar la agilidad en la gestión de proyectos y maximizar la eficiencia de nuestros recursos. Por lo tanto, se recomienda encarecidamente la exploración y desarrollo de agentes de IA como un elemento esencial en nuestra estrategia de crecimiento y mejora continua.
# Demo 2: debate entre agentes (multi-agente para verificacion)

def debate_agentes(pregunta, n_rondas=2, modelo=DEFAULT_MODEL):
    """
    Mejora la precision mediante debate entre multiples agentes.

    Proceso (Wang et al., 2023):
    1. Multiples agentes generan respuestas independientes.
    2. Cada agente lee las respuestas de los demas.
    3. Los agentes actualizan sus respuestas.
    4. Se repite hasta convergencia o max_rondas.

    La probabilidad de error disminuye con cada ronda si el debate es informativo.
    """
    separador(f'DEBATE — {pregunta}')

    n_agentes = 3
    historial_debate = {i: [] for i in range(n_agentes)}
    roles = ['escéptico analitico', 'optimista pragmatico', 'neutral tecnico']

    print(f'Debate con {n_agentes} agentes, {n_rondas} rondas\n')

    for ronda in range(n_rondas):
        print(f'{BOLD}[Ronda {ronda+1}/{n_rondas}]{RESET}')
        respuestas_ronda = []

        for i in range(n_agentes):
            # Contexto: respuestas de otros agentes en rondas anteriores
            otras_respuestas = ''
            if ronda > 0:
                otras_resp_list = []
                for j in range(n_agentes):
                    if j != i and historial_debate[j]:
                        otras_resp_list.append(f'Agente {j+1}: {historial_debate[j][-1]}')
                if otras_resp_list:
                    otras_respuestas = ('\n\nRespuestas de otros agentes (considera estas para actualizar tu posicion):\n'
                                       + '\n'.join(otras_resp_list))

            resp = client.chat.completions.create(
                model=modelo,
                messages=[
                    {'role':'system','content':f'Eres un experto con perspectiva {roles[i]}. '
                     'Razona con rigor y actualiza tu posicion si otros argumentos son validos.' + otras_respuestas},
                    {'role':'user','content': pregunta + ('\n\nReconsidera tu respuesta si es necesario.' if ronda > 0 else '')}
                ],
                max_tokens=200, temperature=0.6,
            )
            texto = resp.choices[0].message.content
            historial_debate[i].append(texto)
            respuestas_ronda.append(texto)
            print(f'  {AZUL}Agente {i+1} ({roles[i]}){RESET}: {texto[:120]}...')

    # Arbitro: sintetiza el debate y da la respuesta final
    print(f'\n{BOLD}[Arbitro]{RESET} Sintetizando el debate...')
    arbitro = client.chat.completions.create(
        model=modelo,
        messages=[
            {'role':'system','content':'Eres un arbitro imparcial. Analiza el debate y da la respuesta mas precisa y equilibrada.'},
            {'role':'user','content':
             f'Pregunta: {pregunta}\n\n' +
             '\n'.join(f'Agente {i+1} (rondas): {" | ".join(historial_debate[i])}' for i in range(n_agentes))}
        ],
        max_tokens=250, temperature=0.2,
    )
    veredicto = arbitro.choices[0].message.content
    imprimir_respuesta(veredicto)
    return veredicto


debate_agentes('Las arquitecturas de agentes autonomos con LLMs ya estan listas para produccion en sistemas criticos como salud o finanzas?')
────────────────────────────────────────────────────────────
  DEBATE — Las arquitecturas de agentes autonomos con LLMs ya estan listas para produccion en sistemas criticos como salud o finanzas?
────────────────────────────────────────────────────────────
Debate con 3 agentes, 2 rondas

[Ronda 1/2]
  Agente 1 (escéptico analitico): La implementación de arquitecturas de agentes autónomos basadas en modelos de lenguaje (LLMs) en sistemas críticos como ...
  Agente 2 (optimista pragmatico): La implementación de arquitecturas de agentes autónomos que utilizan Modelos de Lenguaje Grande (LLMs) en sistemas críti...
  Agente 3 (neutral tecnico): La implementación de arquitecturas de agentes autónomos basadas en Modelos de Lenguaje Grande (LLMs) en sistemas crítico...
[Ronda 2/2]
  Agente 1 (escéptico analitico): La implementación de arquitecturas de agentes autónomos basados en Modelos de Lenguaje Grande (LLMs) en sistemas crítico...
  Agente 2 (optimista pragmatico): La implementación de arquitecturas de agentes autónomos basados en Modelos de Lenguaje Grande (LLMs) en sistemas crítico...
  Agente 3 (neutral tecnico): La implementación de arquitecturas de agentes autónomos basados en Modelos de Lenguaje Grande (LLMs) en sistemas crítico...

[Arbitro] Sintetizando el debate...
Respuesta Final:
y recursos para que los profesionales se concentren en decisiones más complejas y críticas. Esto podría resultar en una mejora en la atención al paciente y en la gestión financiera, optimizando así los procesos operativos en estos sectores.

### En contra de la implementación inmediata:

1. **Limitaciones en la Razonabilidad y Comprensión Contextual**: Aunque los LLMs son efectivos en el procesamiento de lenguaje, su capacidad para razonar de manera consistente y comprender contextos complejos es limitada. En sistemas críticos, donde las decisiones pueden tener un impacto significativo, esta falta de razonamiento profundo puede ser problemática.

2. **Fiabilidad y Seguridad**: La posibilidad de que los LLMs generen respuestas incorrectas o inapropiadas es una preocupación seria. En el ámbito de la salud, un diagnóstico erróneo o un consejo médico inadecuado puede tener consecuencias fatales. Por lo tanto, es crucial realizar una validación rigurosa y pruebas exhaustivas antes de implementar estos sistemas en producción.

3. **Transparencia y Explicabilidad**: La falta de explicabilidad en las decisiones tomadas por los LLMs es un obstáculo importante. Los profesionales en salud y finanzas necesitan entender el razonamiento detrás
'y recursos para que los profesionales se concentren en decisiones más complejas y críticas. Esto podría resultar en una mejora en la atención al paciente y en la gestión financiera, optimizando así los procesos operativos en estos sectores.\n\n### En contra de la implementación inmediata:\n\n1. **Limitaciones en la Razonabilidad y Comprensión Contextual**: Aunque los LLMs son efectivos en el procesamiento de lenguaje, su capacidad para razonar de manera consistente y comprender contextos complejos es limitada. En sistemas críticos, donde las decisiones pueden tener un impacto significativo, esta falta de razonamiento profundo puede ser problemática.\n\n2. **Fiabilidad y Seguridad**: La posibilidad de que los LLMs generen respuestas incorrectas o inapropiadas es una preocupación seria. En el ámbito de la salud, un diagnóstico erróneo o un consejo médico inadecuado puede tener consecuencias fatales. Por lo tanto, es crucial realizar una validación rigurosa y pruebas exhaustivas antes de implementar estos sistemas en producción.\n\n3. **Transparencia y Explicabilidad**: La falta de explicabilidad en las decisiones tomadas por los LLMs es un obstáculo importante. Los profesionales en salud y finanzas necesitan entender el razonamiento detrás'

PARTE 9 — Agentes con Ollama (Modelos Locales)#

Ollama permite correr modelos LLM localmente en Colab sin costo por token y con privacidad total. Los mismos patrones de agentes funcionan con modelos locales.

# ── INSTALACION DE OLLAMA ─────────────────────────────────────
# 1. Descargar e instalar el binario de Ollama.
# 2. Iniciar el servidor en segundo plano (puerto 11434).
# 3. Descargar modelos.

print('Instalando Ollama...')
# Install zstd, as suggested by the ollama install script output
!sudo apt-get update && sudo apt-get install -y zstd
!curl -fsSL https://ollama.com/install.sh | sh
print('Ollama instalado.')
Instalando Ollama...
Get:1 https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/ InRelease [3,632 B]
Get:2 https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64  InRelease [1,581 B]
Get:3 https://cli.github.com/packages stable InRelease [3,917 B]
Get:4 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB]
Get:5 https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/ Packages [91.2 kB]
Get:6 https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64  Packages [2,615 kB]
Hit:7 http://archive.ubuntu.com/ubuntu jammy InRelease
Get:8 https://r2u.stat.illinois.edu/ubuntu jammy InRelease [6,555 B]
Get:9 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [128 kB]
Get:10 http://security.ubuntu.com/ubuntu jammy-security/universe amd64 Packages [1,294 kB]
Get:11 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages [7,004 kB]
Get:12 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages [3,915 kB]
Get:13 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy InRelease [18.1 kB]
Get:14 https://r2u.stat.illinois.edu/ubuntu jammy/main amd64 Packages [3,001 kB]
Hit:15 https://ppa.launchpadcontent.net/graphics-drivers/ppa/ubuntu jammy InRelease
Get:16 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [127 kB]
Hit:17 https://ppa.launchpadcontent.net/ubuntugis/ppa/ubuntu jammy InRelease
Get:18 http://archive.ubuntu.com/ubuntu jammy-updates/restricted amd64 Packages [7,251 kB]
Get:19 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy/main amd64 Packages [38.9 kB]
Get:20 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages [1,602 kB]
Get:21 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [4,247 kB]
Get:22 https://r2u.stat.illinois.edu/ubuntu jammy/main all Packages [10.2 MB]
Fetched 41.6 MB in 3s (13.8 MB/s)
Reading package lists... Done
W: Skipping acquire of configured file 'main/source/Sources' as repository 'https://r2u.stat.illinois.edu/ubuntu jammy InRelease' does not seem to provide it (sources.list entry misspelt?)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  zstd
0 upgraded, 1 newly installed, 0 to remove and 106 not upgraded.
Need to get 603 kB of archives.
After this operation, 1,695 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 zstd amd64 1.4.8+dfsg-3build1 [603 kB]
Fetched 603 kB in 0s (9,703 kB/s)
debconf: unable to initialize frontend: Dialog
debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 78, <> line 1.)
debconf: falling back to frontend: Readline
debconf: unable to initialize frontend: Readline
debconf: (This frontend requires a controlling tty.)
debconf: falling back to frontend: Teletype
dpkg-preconfigure: unable to re-open stdin: 
Selecting previously unselected package zstd.
(Reading database ... 122402 files and directories currently installed.)
Preparing to unpack .../zstd_1.4.8+dfsg-3build1_amd64.deb ...
Unpacking zstd (1.4.8+dfsg-3build1) ...
Setting up zstd (1.4.8+dfsg-3build1) ...
Processing triggers for man-db (2.10.2-1) ...
>>> Cleaning up old version at /usr/local/lib/ollama
>>> Installing ollama to /usr/local
>>> Downloading ollama-linux-amd64.tar.zst
######################################################################## 100.0%
>>> Creating ollama user...
>>> Adding ollama user to video group...
>>> Adding current user to ollama group...
>>> Creating ollama systemd service...
WARNING: systemd is not running
WARNING: Unable to detect NVIDIA/AMD GPU. Install lspci or lshw to automatically detect and install GPU dependencies.
>>> The Ollama API is now available at 127.0.0.1:11434.
>>> Install complete. Run "ollama" from the command line.
Ollama instalado.
import subprocess, time

def iniciar_ollama():
    """Inicia el servidor Ollama en un hilo daemon."""
    # Ensure ollama is in PATH or specify full path if known
    subprocess.Popen(['/usr/local/bin/ollama', 'serve'], # Assuming install.sh puts it here
                     stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

hilo = threading.Thread(target=iniciar_ollama, daemon=True)
hilo.start()
print('Iniciando servidor Ollama...')
time.sleep(5) # Give it a bit more time to start

try:
    r = requests.get('http://localhost:11434', timeout=5)
    print(f'Servidor Ollama activo: {r.text}')
except Exception as e:
    print(f'Error al conectar: {e}')
Iniciando servidor Ollama...
Servidor Ollama activo: Ollama is running
# Descargar modelo de texto para agentes
# llama3.2:1b -> ~800MB, rapido en CPU, calidad basica
# llama3.1:8b -> ~5GB,  requiere GPU, alta calidad
# mistral:7b  -> ~4GB,  bueno en razonamiento
# qwen2.5:3b  -> ~2GB,  buen balance velocidad/calidad

print('Descargando llama3.2:1b (~800MB)...')
!ollama pull llama3.2:1b

OLLAMA_MODEL = 'llama3.2:1b'
OLLAMA_URL   = 'http://localhost:11434'

# Cliente OpenAI apuntando a Ollama (compatible con la API de OpenAI)
client_ollama = OpenAI(base_url=f'{OLLAMA_URL}/v1', api_key='ollama')

print('\nModelos disponibles en Ollama:')
!ollama list
Descargando llama3.2:1b (~800MB)...
?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l?2026h?25l?25h?2026l

Modelos disponibles en Ollama:
NAME           ID              SIZE      MODIFIED               
llama3.2:1b    baf6a787fdff    1.3 GB    Less than a second ago    
# ── AGENTE REACT CON OLLAMA ───────────────────────────────────
#
# Ollama es compatible con la API de OpenAI, PERO muchos modelos
# abiertos NO soportan Function Calling nativo.
# Solucion: usar ReAct con parsing manual (mismo patron que la Parte 4).
#
# Diferencias respecto a GPT:
# - Sin Function Calling automatico: parseamos la salida manualmente.
# - Modelos mas pequeños: respuestas menos precisas, mas errores de formato.
# - Sin costo: ideal para desarrollo y experimentos.
# - Privacidad: los datos no salen del servidor.

def ollama_chat(mensajes, modelo=OLLAMA_MODEL, temperature=0.3, max_tokens=500, stop=None):
    """
    Llamada al modelo Ollama via API REST nativa.
    Mas flexible que el cliente OpenAI para opciones especificas de Ollama.
    """
    payload = {
        'model'   : modelo,
        'messages': mensajes,
        'stream'  : False,
        'options' : {'temperature': temperature, 'num_predict': max_tokens},
    }
    if stop:
        payload['options']['stop'] = stop

    resp = requests.post(f'{OLLAMA_URL}/api/chat', json=payload, timeout=120)
    return resp.json()['message']['content']


def ollama_react(tarea, registro, modelo=OLLAMA_MODEL, max_pasos=8):
    """
    Agente ReAct usando Ollama (modelo local).

    El patron es identico al AgenteReAct de la Parte 4, pero:
    1. Usa ollama_chat() en lugar del cliente OpenAI.
    2. El prompt es mas explicito para modelos pequeños.
    3. El parsing es mas tolerante a errores de formato.
    """
    separador(f'ReAct OLLAMA [{modelo}] — {tarea}')

    prompt_sistema = f"""Eres un agente que resuelve tareas con herramientas.

FORMATO OBLIGATORIO para cada paso:
Pensamiento: <tu razonamiento>
Accion: nombre_herramienta({{'param': 'valor'}})

Cuando termines:
Pensamiento: <conclusion>
Respuesta Final: <respuesta>

Herramientas:
- buscar_web({{'query': 'texto'}})
- calcular({{'expresion': '2+2'}})
- ejecutar_python({{'codigo': 'print(42)'}})
- obtener_hora_fecha()
- leer_base_datos({{'tabla': 'proyectos'}})

Usa JSON valido con comillas dobles en los argumentos.
"""

    mensajes = [
        {'role': 'system', 'content': prompt_sistema},
        {'role': 'user',   'content': f'Tarea: {tarea}'}
    ]

    for paso in range(max_pasos):
        salida = ollama_chat(mensajes, modelo=modelo, stop=['Observacion:'])
        salida = salida.strip()

        if 'Respuesta Final:' in salida:
            idx  = salida.index('Respuesta Final:')
            pens = salida[:idx].replace('Pensamiento:','').strip()
            resp = salida[idx+len('Respuesta Final:'):].strip()
            if pens: imprimir_pensamiento(f'[Ollama] {pens}')
            imprimir_respuesta(f'[Ollama] {resp}')
            return resp

        if 'Pensamiento:' in salida:
            pens = salida.split('Accion:')[0].replace('Pensamiento:','').strip()
            imprimir_pensamiento(f'[Ollama] {pens}')

        # Parsear la accion del texto generado
        accion_match = re.search(r'Accion:\s*(\w+)\((.*)\)', salida, re.DOTALL)
        if accion_match:
            nombre_h = accion_match.group(1)
            args_str = accion_match.group(2).strip()
            try:
                args = json.loads(args_str) if args_str else {}
            except Exception:
                args = {}
            imprimir_accion(f'[Ollama] {nombre_h}', args)
            obs = registro.ejecutar(nombre_h, args)
            imprimir_observacion(obs)
            # Añadir T+A+O al historial
            contenido = f'{salida}\nObservacion: {json.dumps(obs, ensure_ascii=False, default=str)}'
            mensajes.append({'role': 'assistant', 'content': contenido})
        else:
            mensajes.append({'role': 'assistant', 'content': salida})

    return 'Max pasos alcanzados.'


# Demo ReAct con Ollama
ollama_react('Calcula cuanto es 15 al cuadrado mas 200 dividido entre 5.', registro)
────────────────────────────────────────────────────────────
  ReAct OLLAMA [llama3.2:1b] — Calcula cuanto es 15 al cuadrado mas 200 dividido entre 5.
────────────────────────────────────────────────────────────
Pensamiento: [Ollama] Para calcular la expresión, primero debemos realizar las operaciones aritméticas y luego aplicar el orden de precedencia.

Calculo:
1. El primer paso es calcular 15 al cuadrado, que equivale a 225.
2. Luego, sumamos 200 a esa cantidad, lo que da como resultado 425.
3. Finalmente, dividimos ese resultado entre 5, lo que nos da 85.

 La expresión resultante es 85.

Accion: calcular({'expresion': '225+200/5'})
Respuesta Final:
[Ollama] 85
'85'
# Demo Ollama: agente de conversacion con memoria
def agente_ollama_memoria(tarea, historial_previo=None, modelo=OLLAMA_MODEL):
    """
    Agente conversacional con Ollama y memoria de corto plazo.
    Mismo patron que en la Parte 2 pero con el modelo local.
    """
    mensajes = [
        {'role':'system','content':'Eres un asistente de IA util y conciso. Responde en espanol.'}
    ]
    if historial_previo:
        mensajes.extend(historial_previo)
    mensajes.append({'role':'user','content':tarea})

    respuesta = ollama_chat(mensajes, modelo=modelo, max_tokens=300)
    return respuesta, mensajes + [{'role':'assistant','content':respuesta}]


separador('Conversacion con historial — Ollama')

hist = None
preguntas = [
    'Que es un agente de IA en una frase?',
    'Y que es ReAct en el contexto de agentes?',
    'Dame un ejemplo concreto de uso de lo que explicaste.',
]

for p in preguntas:
    print(f'{BOLD}Usuario:{RESET} {p}')
    resp, hist = agente_ollama_memoria(p, hist[1:] if hist else None)
    print(f'{AZUL}Ollama:{RESET} {resp}\n')
────────────────────────────────────────────────────────────
  Conversacion con historial — Ollama
────────────────────────────────────────────────────────────
Usuario: Que es un agente de IA en una frase?
Ollama: Un agente de Inteligencia Artificial (IA) es una entidad que puede realizar tareas, tomar decisiones y actuar de manera independiente, utilizando algoritmos y conocimientos para resolver problemas o cumplir con objetivos.

Usuario: Y que es ReAct en el contexto de agentes?
Ollama: En el contexto de los agentes de Inteligencia Artificial (IA), "ReAct" se refiere a una tecnología que permite al agente tomar decisiones y acciones basadas en la retroalimentación recibida del entorno, lo que le permite adaptarse y mejorar su comportamiento a medida que avanza.

Usuario: Dame un ejemplo concreto de uso de lo que explicaste.
Ollama: Un ejemplo real de ReAct es el sistema de seguimiento de la salud de un paciente en hospicio, donde los agentes de IA utilizan datos de sensorias y retroalimentación para ajustar su plan de tratamiento y asegurarse de que el paciente reciba el mejor cuidado posible.

Por ejemplo, si un agente ReAct detecta que el paciente está experimentando una disminución en la frecuencia cardíaca, puede reaccionar activando un sistema de alerta que envía una notificación a los enfermeros para que revisen la situación y ajusten su plan de tratamiento. De esta manera, el agente ReAct puede tomar medidas inmediatas para asegurarse de que el paciente reciba el cuidado necesario.

Este tipo de uso de ReAct permite al agente adaptarse y mejorar su comportamiento a medida que avanza, lo que es especialmente útil en entornos complejos como hospitales.

PARTE 10 — Comparativa y Guia de Seleccion#

def benchmark_agentes(tarea, registro, modelo_openai=DEFAULT_MODEL, modelo_ollama=OLLAMA_MODEL):
    """
    Compara GPT vs Ollama en la misma tarea midiendo tiempo y calidad.
    """
    separador(f'BENCHMARK — {tarea}')

    # OpenAI GPT
    print(f'{BOLD}[GPT — {modelo_openai}]{RESET}')
    t0    = time.time()
    resp_gpt = agente_con_herramientas(tarea, registro, modelo=modelo_openai, verbose=True)
    t_gpt = time.time() - t0
    print(f'Tiempo GPT: {t_gpt:.1f}s')

    print()

    # Ollama local
    print(f'{BOLD}[Ollama — {modelo_ollama}]{RESET}')
    t0 = time.time()
    resp_ollama = ollama_react(tarea, registro, modelo=modelo_ollama)
    t_ollama = time.time() - t0
    print(f'Tiempo Ollama: {t_ollama:.1f}s')

    print(f'\n{BOLD}Resumen:{RESET}')
    print(f'  GPT    : {t_gpt:.1f}s')
    print(f'  Ollama : {t_ollama:.1f}s (x{t_ollama/t_gpt:.1f} vs GPT)')


# Comparativa en tarea numerica
benchmark_agentes('Calcula cuanto es (2**10 + 3**5) * 7 y dime si el resultado es par o impar.', registro)
────────────────────────────────────────────────────────────
  BENCHMARK — Calcula cuanto es (2**10 + 3**5) * 7 y dime si el resultado es par o impar.
────────────────────────────────────────────────────────────
[GPT — gpt-4o-mini]

────────────────────────────────────────────────────────────
  TAREA: Calcula cuanto es (2**10 + 3**5) * 7 y dime si el resultado es par o impar.
────────────────────────────────────────────────────────────
Accion: calcular({"expresion": "(2**10 + 3**5) * 7"})
Observacion: {'expresion': '(2**10 + 3**5) * 7', 'resultado': 8869}
Accion: ejecutar_python({"codigo": "resultado = 8869\nif resultado % 2 == 0:\n    par_impar = 'par'\nelse:\n    par_impar = 'impar'\npar_impar"})
Observacion: Codigo ejecutado sin salida.
Respuesta Final:
El cálculo de \((2^{10} + 3^{5}) \times 7\) da como resultado **8869**. 

El resultado **8869** es **impar**.
Tiempo GPT: 3.8s

[Ollama — llama3.2:1b]

────────────────────────────────────────────────────────────
  ReAct OLLAMA [llama3.2:1b] — Calcula cuanto es (2**10 + 3**5) * 7 y dime si el resultado es par o impar.
────────────────────────────────────────────────────────────
Pensamiento: [Ollama] Para calcular el resultado, necesitamos realizar las operaciones aritméticas dentro de la expresión dada.
Accion: [Ollama] buscar_web({})
Observacion: Error al ejecutar buscar_web: buscar_web() missing 1 required positional argument: 'query'
Respuesta Final:
[Ollama] La respuesta es un número entero, por lo que no es par ni impar.
Tiempo Ollama: 3.4s

Resumen:
  GPT    : 3.8s
  Ollama : 3.4s (x0.9 vs GPT)
# Tabla comparativa final
tabla = '''
╔══════════════════════╦══════════════════════════════╦══════════════════════════════╗
║ Aspecto              ║ OpenAI GPT-4o / 4o-mini      ║ Ollama (local)               ║
╠══════════════════════╬══════════════════════════════╬══════════════════════════════╣
║ Costo                ║ Por token (~$0.15/1M tokens) ║ Gratis (solo hardware)       ║
║ Privacidad           ║ Datos van a OpenAI           ║ 100% local, sin salida red   ║
║ Calidad razonamiento ║ SOTA                         ║ Depende del modelo elegido   ║
║ Function Calling     ║ Nativo y robusto             ║ Parcial (modelos recientes)  ║
║ ReAct                ║ Muy preciso                  ║ Funciona con prompt correcto ║
║ Velocidad (CPU)      ║ Muy rapido (GPUs dedicadas)  ║ Lento (modelo 7B: ~30s/resp) ║
║ Velocidad (GPU T4)   ║ Muy rapido                   ║ Rapido (modelo 7B: ~5s/resp) ║
║ Contexto max         ║ 128K tokens                  ║ Varia por modelo (4K-128K)   ║
║ Modelos disponibles  ║ GPT-4o, 4o-mini, o1, etc.   ║ LLaMA, Mistral, Qwen, etc.  ║
║ Setup                ║ Solo API key                 ║ Instalar + descargar modelos ║
║ Offline              ║ No                           ║ Si (despues de descargar)    ║
╚══════════════════════╩══════════════════════════════╩══════════════════════════════╝
'''
print(tabla)

guia = '''
CUANDO USAR CADA UNO:
─────────────────────────────────────────────

USA OpenAI GPT cuando:
  ✓ Necesitas la mayor precision en razonamiento complejo.
  ✓ Trabajas en produccion con usuarios reales.
  ✓ Usas Function Calling intensivamente.
  ✓ La latencia es critica.
  ✓ No tienes GPU disponible.

USA Ollama cuando:
  ✓ Los datos son confidenciales (salud, legal, finanzas).
  ✓ Quieres costo cero en inferencia (muchas consultas).
  ✓ Estas en fase de desarrollo y prototipado.
  ✓ Necesitas trabajar offline.
  ✓ Quieres experimentar con arquitecturas de agentes.
  ✓ Tienes acceso a GPU (A100, H100) para modelos grandes.

PATRON HIBRIDO (recomendado en produccion):
  - Ollama para tareas de clasificacion/routing (economico).
  - GPT-4o para razonamiento complejo y generacion final (calidad).
  - Embeddings OpenAI para memoria semantica (precision).
'''
print(guia)
╔══════════════════════╦══════════════════════════════╦══════════════════════════════╗
║ Aspecto              ║ OpenAI GPT-4o / 4o-mini      ║ Ollama (local)               ║
╠══════════════════════╬══════════════════════════════╬══════════════════════════════╣
║ Costo                ║ Por token (~$0.15/1M tokens) ║ Gratis (solo hardware)       ║
║ Privacidad           ║ Datos van a OpenAI           ║ 100% local, sin salida red   ║
║ Calidad razonamiento ║ SOTA                         ║ Depende del modelo elegido   ║
║ Function Calling     ║ Nativo y robusto             ║ Parcial (modelos recientes)  ║
║ ReAct                ║ Muy preciso                  ║ Funciona con prompt correcto ║
║ Velocidad (CPU)      ║ Muy rapido (GPUs dedicadas)  ║ Lento (modelo 7B: ~30s/resp) ║
║ Velocidad (GPU T4)   ║ Muy rapido                   ║ Rapido (modelo 7B: ~5s/resp) ║
║ Contexto max         ║ 128K tokens                  ║ Varia por modelo (4K-128K)   ║
║ Modelos disponibles  ║ GPT-4o, 4o-mini, o1, etc.   ║ LLaMA, Mistral, Qwen, etc.  ║
║ Setup                ║ Solo API key                 ║ Instalar + descargar modelos ║
║ Offline              ║ No                           ║ Si (despues de descargar)    ║
╚══════════════════════╩══════════════════════════════╩══════════════════════════════╝


CUANDO USAR CADA UNO:
─────────────────────────────────────────────

USA OpenAI GPT cuando:
  ✓ Necesitas la mayor precision en razonamiento complejo.
  ✓ Trabajas en produccion con usuarios reales.
  ✓ Usas Function Calling intensivamente.
  ✓ La latencia es critica.
  ✓ No tienes GPU disponible.

USA Ollama cuando:
  ✓ Los datos son confidenciales (salud, legal, finanzas).
  ✓ Quieres costo cero en inferencia (muchas consultas).
  ✓ Estas en fase de desarrollo y prototipado.
  ✓ Necesitas trabajar offline.
  ✓ Quieres experimentar con arquitecturas de agentes.
  ✓ Tienes acceso a GPU (A100, H100) para modelos grandes.

PATRON HIBRIDO (recomendado en produccion):
  - Ollama para tareas de clasificacion/routing (economico).
  - GPT-4o para razonamiento complejo y generacion final (calidad).
  - Embeddings OpenAI para memoria semantica (precision).
# RESUMEN DE PATRONES DE AGENTES IMPLEMENTADOS

resumen = '''
PATRONES DE AGENTES IMPLEMENTADOS EN ESTE NOTEBOOK
═══════════════════════════════════════════════════

1. AGENTE BASICO
   Patron: Percibir -> Razonar (LLM) -> Actuar -> Observar
   Memoria: historial de mensajes (ventana de contexto)
   Uso: chatbots, asistentes conversacionales

2. FUNCTION CALLING (OpenAI nativo)
   Patron: LLM decide herramienta + args -> ejecutar -> retornar
   Herramientas: busqueda, calculo, BD, codigo, APIs
   Uso: agentes que necesitan interactuar con el mundo

3. ReAct (Razonamiento + Accion)
   Patron: T1 -> A1 -> O1 -> T2 -> A2 -> O2 -> Respuesta
   Clave: stop=["Observacion:"] evita que el LLM invente resultados
   Uso: tareas de busqueda multi-paso, razonamiento verificable

4. REFLEXION
   Patron: Intento -> Evaluar -> Reflexionar -> Reintentar
   Mejora: P(exito_T) >= P(exito_{T-1}) si reflexion es informativa
   Uso: tareas con criterio de exito verificable

5. MEMORIA DE DOS NIVELES
   Corto plazo: ventana de contexto (FIFO, compresion por resumen)
   Largo plazo: vectores de embeddings con recuperacion semantica
   Recuperacion: score = a*recencia + b*importancia + c*relevancia
   Uso: asistentes personales, agentes con historial largo

6. PLANIFICACION (CoT + HTN)
   CoT: razonamiento paso a paso antes de responder
   HTN: Tarea -> (T1, T2, ..., Tk) -> resolver cada subtarea
   Uso: tareas complejas que requieren multiples pasos

7. MULTI-AGENTE (Orquestador + Especialistas)
   Topologia: estrella (orquestador central)
   Pipeline: Investigador -> Analista -> Escritor -> Critico
   Contexto: encadenado entre especialistas
   Uso: tareas que requieren experticias diversas

8. DEBATE MULTI-AGENTE
   Patron: N agentes independientes -> rondas de discusion -> arbitro
   Mejora: P(error) disminuye con rondas si el debate es informativo
   Uso: preguntas donde la precision es critica

9. AGENTES CON OLLAMA (local)
   ReAct manual: mismo patron pero parsing de texto en lugar de JSON
   Privacidad: datos nunca salen del servidor
   Uso: datos sensibles, desarrollo sin costo
'''
print(resumen)
PATRONES DE AGENTES IMPLEMENTADOS EN ESTE NOTEBOOK
═══════════════════════════════════════════════════

1. AGENTE BASICO
   Patron: Percibir -> Razonar (LLM) -> Actuar -> Observar
   Memoria: historial de mensajes (ventana de contexto)
   Uso: chatbots, asistentes conversacionales

2. FUNCTION CALLING (OpenAI nativo)
   Patron: LLM decide herramienta + args -> ejecutar -> retornar
   Herramientas: busqueda, calculo, BD, codigo, APIs
   Uso: agentes que necesitan interactuar con el mundo

3. ReAct (Razonamiento + Accion)
   Patron: T1 -> A1 -> O1 -> T2 -> A2 -> O2 -> Respuesta
   Clave: stop=["Observacion:"] evita que el LLM invente resultados
   Uso: tareas de busqueda multi-paso, razonamiento verificable

4. REFLEXION
   Patron: Intento -> Evaluar -> Reflexionar -> Reintentar
   Mejora: P(exito_T) >= P(exito_{T-1}) si reflexion es informativa
   Uso: tareas con criterio de exito verificable

5. MEMORIA DE DOS NIVELES
   Corto plazo: ventana de contexto (FIFO, compresion por resumen)
   Largo plazo: vectores de embeddings con recuperacion semantica
   Recuperacion: score = a*recencia + b*importancia + c*relevancia
   Uso: asistentes personales, agentes con historial largo

6. PLANIFICACION (CoT + HTN)
   CoT: razonamiento paso a paso antes de responder
   HTN: Tarea -> (T1, T2, ..., Tk) -> resolver cada subtarea
   Uso: tareas complejas que requieren multiples pasos

7. MULTI-AGENTE (Orquestador + Especialistas)
   Topologia: estrella (orquestador central)
   Pipeline: Investigador -> Analista -> Escritor -> Critico
   Contexto: encadenado entre especialistas
   Uso: tareas que requieren experticias diversas

8. DEBATE MULTI-AGENTE
   Patron: N agentes independientes -> rondas de discusion -> arbitro
   Mejora: P(error) disminuye con rondas si el debate es informativo
   Uso: preguntas donde la precision es critica

9. AGENTES CON OLLAMA (local)
   ReAct manual: mismo patron pero parsing de texto en lugar de JSON
   Privacidad: datos nunca salen del servidor
   Uso: datos sensibles, desarrollo sin costo