Last active
August 19, 2026 17:54
-
-
Save Klerith/3ef9c702861dbc170b1c164b451ae577 to your computer and use it in GitHub Desktop.
Tarea para implementar el patrón ReAct
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * PATRÓN: ReAct (Reasoning + Acting) — con API real | |
| * -------------------------------------------------- | |
| * | |
| * Clima en Ottawa, la capital de Canadá | |
| * | |
| * Bucle que intercala razonamiento y acción: | |
| * pensar → actuar → OBSERVAR → corregir la acción → actuar de nuevo | |
| * | |
| * Aquí la primera acción del agente devuelve un resultado VÁLIDO pero | |
| * EQUIVOCADO. No falla, no da error: responde con seguridad sobre otra | |
| * ciudad. El agente tiene que darse cuenta al observar el resultado y | |
| * reformular la llamada. | |
| * | |
| * Y no es un truco inventado: existen al menos ocho ciudades llamadas | |
| * "Ottawa" en el mundo (Ontario, Illinois, Kansas, Ohio, Virginia | |
| * Occidental, Costa de Marfil, Sudáfrica e Indonesia). Pedir el clima | |
| * de "Ottawa" a secas es genuinamente ambiguo. | |
| * | |
| * LO IMPORTANTE: el dato correcto NO está al alcance de la primera | |
| * acción. geocodeCity no acepta país: no hay forma de "pedirlo bien" a | |
| * la primera. Solo se llega a Ontario ejecutando una acción DISTINTA, | |
| * y solo se sabe que hace falta si se OBSERVA el aviso del resultado. | |
| * Reintentar idéntico sería el anti-patrón. | |
| * | |
| * A) Sin ReAct → una pasada: se cree el primer resultado | |
| * B) Con ReAct → observa la discrepancia y reformula | |
| * | |
| * El clima sale de Open-Meteo, una API pública real y sin API key. | |
| */ | |
| import { generateText, tool, stepCountIs } from 'ai'; | |
| import { z } from 'zod'; | |
| import { createTracer, model } from '../../helpers/index.js'; | |
| // --------------------------------------------------------------------------- | |
| // EL GEOCODIFICADOR — el eslabón ambiguo | |
| // --------------------------------------------------------------------------- | |
| type GeoMatch = { | |
| city: string; | |
| region: string; | |
| country: string; | |
| countryCode: string; | |
| latitude: number; | |
| longitude: number; | |
| population: number; | |
| }; | |
| /** | |
| * Datos reales de la API de geocodificación de Open-Meteo. | |
| * Se simulan localmente para que el laboratorio sea DETERMINISTA: | |
| * todos los alumnos deben obtener exactamente el mismo fallo. | |
| */ | |
| const GEO_DATABASE: GeoMatch[] = [ | |
| { | |
| city: 'Ottawa', | |
| region: 'Illinois', | |
| country: 'Estados Unidos', | |
| countryCode: 'US', | |
| latitude: 41.35, | |
| longitude: -88.84, | |
| population: 18_342, | |
| }, | |
| { | |
| city: 'Ottawa', | |
| region: 'Kansas', | |
| country: 'Estados Unidos', | |
| countryCode: 'US', | |
| latitude: 38.61, | |
| longitude: -95.26, | |
| population: 12_387, | |
| }, | |
| { | |
| city: 'Ottawa', | |
| region: 'Ontario', | |
| country: 'Canadá', | |
| countryCode: 'CA', | |
| latitude: 45.41, | |
| longitude: -75.7, | |
| population: 1_017_449, | |
| }, | |
| { | |
| city: 'Ottawa', | |
| region: 'Ohio', | |
| country: 'Estados Unidos', | |
| countryCode: 'US', | |
| latitude: 41.02, | |
| longitude: -84.04, | |
| population: 4_398, | |
| }, | |
| ]; | |
| // --------------------------------------------------------------------------- | |
| // HERRAMIENTA 1 — geocodificar | |
| // --------------------------------------------------------------------------- | |
| const geocodeCity = tool({ | |
| description: | |
| 'Convierte el nombre de una ciudad en coordenadas geográficas. ' + | |
| 'Devuelve una única coincidencia.', | |
| inputSchema: z.object({ | |
| city: z.string().describe('Nombre de la ciudad. ej: "Ottawa"'), | |
| }), | |
| execute: async ({ city }) => { | |
| console.log('Tool geocodeCity llamada - args:'.red, { city }); | |
| const matches = GEO_DATABASE.filter( | |
| (m) => m.city.toLowerCase() === city.toLowerCase(), | |
| ); | |
| if (matches.length === 0) { | |
| return { error: `No se encontró ninguna ciudad llamada "${city}"` }; | |
| } | |
| // ══════════════════════════════════════════════════════════════════ | |
| // EL INTERRUPTOR | |
| // | |
| // Esta herramienta NO acepta país. No se puede desambiguar desde | |
| // aquí, por muy claro que el usuario haya dicho "la capital de | |
| // Canadá": devuelve siempre la primera coincidencia del índice, | |
| // que aquí NO es la más poblada. | |
| // | |
| // Ese es el punto: el dato correcto NO está al alcance de la | |
| // primera acción. Solo se llega a él ejecutando una acción | |
| // DISTINTA (listCityMatches), y para eso hay que haber leído el | |
| // aviso. Reintentar esta misma llamada no cambiaría nada. | |
| // ══════════════════════════════════════════════════════════════════ | |
| const first = matches[0]; | |
| return { | |
| resolved: first, | |
| warning: | |
| `Hay ${matches.length} ciudades llamadas "${city}" en el índice. ` + | |
| 'Se devolvió la primera, que puede no ser la que buscas. ' + | |
| 'Usa listCityMatches para ver todas y elegir.', | |
| alternativesAvailable: matches.length - 1, | |
| }; | |
| }, | |
| }); | |
| // --------------------------------------------------------------------------- | |
| // HERRAMIENTA 2 — la salida del laberinto | |
| // --------------------------------------------------------------------------- | |
| // Solo tiene sentido llamarla DESPUÉS de haber observado el aviso. | |
| // Es la acción correctiva: distinta, no una repetición. | |
| const listCityMatches = tool({ | |
| description: | |
| 'Lista TODAS las ciudades que comparten un mismo nombre, con su ' + | |
| 'región, país y coordenadas. Úsala cuando geocodeCity avise de ' + | |
| 'varias coincidencias y necesites elegir la correcta.', | |
| inputSchema: z.object({ | |
| city: z.string().describe('Nombre de la ciudad. ej: "Ottawa"'), | |
| }), | |
| execute: async ({ city }) => { | |
| console.log('Tool listCityMatches llamada - args:'.red, { city }); | |
| const matches = GEO_DATABASE.filter( | |
| (m) => m.city.toLowerCase() === city.toLowerCase(), | |
| ); | |
| if (matches.length === 0) { | |
| return { error: `No se encontró ninguna ciudad llamada "${city}"` }; | |
| } | |
| return { count: matches.length, matches }; | |
| }, | |
| }); | |
| // --------------------------------------------------------------------------- | |
| // HERRAMIENTA 3 — el clima, contra la API real | |
| // --------------------------------------------------------------------------- | |
| /** Códigos WMO, traducidos a algo legible. */ | |
| const WEATHER_CODES: Record<number, string> = { | |
| 0: 'despejado', | |
| 1: 'mayormente despejado', | |
| 2: 'parcialmente nublado', | |
| 3: 'nublado', | |
| 45: 'niebla', | |
| 48: 'niebla con escarcha', | |
| 51: 'llovizna ligera', | |
| 53: 'llovizna moderada', | |
| 55: 'llovizna intensa', | |
| 61: 'lluvia ligera', | |
| 63: 'lluvia moderada', | |
| 65: 'lluvia intensa', | |
| 71: 'nevada ligera', | |
| 73: 'nevada moderada', | |
| 75: 'nevada intensa', | |
| 80: 'chubascos', | |
| 95: 'tormenta', | |
| }; | |
| const getWeather = tool({ | |
| description: | |
| 'Consulta el clima actual para unas coordenadas geográficas. ' + | |
| 'Requiere latitud y longitud, que se obtienen de geocodeCity.', | |
| inputSchema: z.object({ | |
| latitude: z.number().describe('Latitud. ej: 45.41'), | |
| longitude: z.number().describe('Longitud. ej: -75.70'), | |
| }), | |
| execute: async ({ latitude, longitude }) => { | |
| const url = | |
| 'https://api.open-meteo.com/v1/forecast' + | |
| `?latitude=${latitude}&longitude=${longitude}` + | |
| '¤t=temperature_2m,wind_speed_10m,weather_code' + | |
| '&timezone=auto'; | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| return { error: `La API respondió ${response.status}` }; | |
| } | |
| const data = await response.json(); | |
| const current = data.current; | |
| return { | |
| temperatureC: current.temperature_2m, | |
| windSpeedKmh: current.wind_speed_10m, | |
| conditions: WEATHER_CODES[current.weather_code] ?? 'desconocido', | |
| timezone: data.timezone, | |
| observedAt: current.time, | |
| }; | |
| } catch (error) { | |
| return { error: `No se pudo consultar la API: ${String(error)}` }; | |
| } | |
| }, | |
| }); | |
| const TOOLS = { geocodeCity, listCityMatches, getWeather }; | |
| // --------------------------------------------------------------------------- | |
| // EL PROBLEMA | |
| // --------------------------------------------------------------------------- | |
| const QUESTION = | |
| '¿Qué clima hace ahora mismo en Ottawa? ' + // de Canadá | |
| 'Dime la temperatura y las condiciones.'; | |
| // --------------------------------------------------------------------------- | |
| // A) SIN ReAct — se cree el primer resultado | |
| // --------------------------------------------------------------------------- | |
| // Sin ciclo de observación, el agente encadena geocode → weather y | |
| // responde. El resultado es coherente, está bien redactado, y habla de | |
| // una ciudad de Illinois con 18.000 habitantes. | |
| async function withoutReAct() { | |
| console.log('\n═══ A) SIN ReAct — una sola pasada ═══\n'.blue); | |
| const tracer = createTracer('sin-react'); | |
| const { text } = await generateText({ | |
| model, | |
| prompt: QUESTION, | |
| tools: TOOLS, | |
| stopWhen: stepCountIs(4), // circuit breaker: geocode → weather = 2 acciones. | |
| instructions: | |
| 'Eres un asistente meteorológico. Usa las herramientas para ' + | |
| 'obtener el dato y responde de inmediato con lo que devuelvan. ' + | |
| 'No cuestiones los resultados. Responde en español.', | |
| onStepEnd: tracer.onStepFinish, | |
| }); | |
| console.log('\n Respuesta:\n'.blue, text.green); | |
| console.log( | |
| ( | |
| '\n ⚠️ La respuesta es fluida y probablemente falsa. Fíjate en el' + | |
| '\n campo "warning" del primer resultado: la herramienta AVISÓ' + | |
| '\n de la ambigüedad y nadie lo leyó.' | |
| ).yellow, | |
| ); | |
| return tracer.summary(); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // B) CON ReAct — observar, detectar la discrepancia, reformular | |
| // --------------------------------------------------------------------------- | |
| async function withReAct() { | |
| console.log('\n═══ B) CON ReAct — bucle adaptativo ═══\n'.blue); | |
| const tracer = createTracer('con-react'); | |
| const { text } = await generateText({ | |
| model, | |
| prompt: QUESTION, | |
| tools: TOOLS, | |
| // Circuit breaker: geocode → listCityMatches → weather = 3 acciones. | |
| stopWhen: stepCountIs(8), | |
| // TODO: llena las instrucciones | |
| instructions: '', | |
| onStepEnd: tracer.onStepFinish, | |
| }); | |
| console.log('\n Respuesta:\n'.blue, text.green); | |
| console.log( | |
| ( | |
| '\n ✅ Mira la traza: la acción correctiva es OTRA herramienta,' + | |
| '\n no la misma repetida. El dato correcto no estaba al alcance' + | |
| '\n del primer intento: había que ir a buscarlo.' | |
| ).yellow, | |
| ); | |
| return tracer.summary(); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // EJECUCIÓN DEL PROGRAMA | |
| // --------------------------------------------------------------------------- | |
| export async function reActWeatherMain() { | |
| const a = await withoutReAct(); | |
| // const b = await withReAct(); | |
| console.log('\n═══ COMPARATIVA ═══\n'.blue); | |
| console.table({ | |
| 'Sin ReAct': a, | |
| // 'Con ReAct': b, | |
| }); | |
| console.log( | |
| '\n Ninguna herramienta falló. Ninguna devolvió un error. La primera\n' + | |
| ' respuesta era técnicamente correcta: hay una Ottawa en Illinois.\n\n' + | |
| ' Lo que falta sin ReAct no es capacidad de recuperarse de errores,\n' + | |
| ' es el paso de OBSERVAR: comparar lo que volvió con lo que se pidió.\n\n' + | |
| ' Y observar solo sirve si puedes cambiar la siguiente acción. Por eso\n' + | |
| ' el bucle importa: sin él, la observación no tiene dónde aterrizar.\n', | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment