EnCore
Un agente serverless para el
Instituto de Geofísica, UNAM
mx-central-unam-ipn-1
Construyámoslo juntos, acto por acto.
Acto 1 / 5
Una computadora
que no existe
Hasta que alguien la necesita.
Presiónalo varias veces.
// Esta función no existe hasta que alguien la llama.
let arranqueEnFrio = true;
let invocaciones = 0;
export const handler = async (event) => {
const frio = arranqueEnFrio;
arranqueEnFrio = false;
invocaciones += 1;
return {
mensaje: "Hola desde una Lambda en us-east-1",
arranqueEnFrio: frio,
// Estas dos sobreviven entre llamadas: AWS
// reutiliza el contenedor. Presiona otra vez
// y velas subir. Es la misma computadora.
contenedorVivoS: process.uptime(),
invocacionesDeEsteContenedor: invocaciones
};
};
# Todo el backend: función, base de datos, permisos
# y endpoint. Un comando lo reconstruye idéntico.
Transform: AWS::Serverless-2016-10-31
Resources:
AgenteFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Architectures: [arm64]
Timeout: 60
MemorySize: 512
FunctionUrlConfig:
AuthType: NONE
# La línea que hace que el chat "escriba"
InvokeMode: RESPONSE_STREAM
# TODO lo que esta función puede hacer.
# Dos permisos. Nada más.
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref EstadoTable
- Statement:
- Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
# Sin esto los logs se guardan —y se cobran—
# para siempre. La fuga más fácil de olvidar.
AgenteLogGroup:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 7
Acto 2 / 5
Le ponemos
un cerebro
Que resulta que no sabe nada.
Sabe hablar. Es distinto.
Bedrock
// Sin herramientas. Sin memoria. Sin acceso a nada.
// Solo el modelo, contestando de lo que recuerda.
const res = await bedrock.send(new ConverseCommand({
modelId: "claude-haiku-4-5",
messages: [{
role: "user",
content: [{ text: pregunta }]
}],
inferenceConfig: { maxTokens: 300 }
}));
return {
texto: res.output.message.content[0].text,
// Bedrock reporta lo que costó la llamada.
// Ese número es el que va al medidor de cuota.
entrada: res.usage.inputTokens,
salida: res.usage.outputTokens
};
Acto 3 / 5
Darle manos
El modelo pide.
Nosotros ejecutamos.
Código nuestro
tool({
name: "buscar_sesiones",
// Esto es lo que lee el modelo para decidir si le
// sirve. No es documentación: es la interfaz.
description:
"Busca sesiones del Summit por tema, nivel o " +
"track. Omite las que ya terminaron.",
inputSchema: z.object({
tema: z.string().optional(),
nivel: z.number().optional()
}),
callback: async ({ tema, nivel }) => {
const hoy = ahora();
return AGENDA.sesiones
.filter((s) => coincide(s, { tema, nivel }))
.filter((s) => estadoSesion(s, hoy.hora) !== "pasada")
.slice(0, 6);
}
});
// El modelo NO ejecuta esto. Pide que se ejecute,
// nosotros lo corremos y le devolvemos el resultado.
// Después vuelve a llamarlo con eso en el contexto.
// Ese ida y vuelta es todo el truco de un agente.
Acto 4 / 5
El modelo
no decide
Nunca.
Y hay que poder demostrarlo.
DynamoDB
// El cupo del stand no lo decide el modelo.
// Tampoco este archivo. Lo decide la base de datos.
try {
await ddb.send(new UpdateCommand({
Key: { id: `slot#${espacio.id}` },
UpdateExpression: "ADD ocupados :uno",
ConditionExpression:
"attribute_not_exists(ocupados) OR ocupados < :cupo",
ExpressionAttributeValues: {
":uno": 1,
":cupo": espacio.cupo
}
}));
estado = "CONFIRMADO";
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
estado = "LISTA_ESPERA";
}
}
// O la condición se cumple y el contador sube, o
// falla y no sube. No hay estado intermedio ni
// ventana de carrera. No existe forma de pedirle a
// la base que confirme un lugar que no hay.
Acto 5 / 5
Se llama
Summitsito
Conoce las 118 sesiones de hoy
y el programa del stand. Pregúntale.
0Invocaciones
0Tokens
$0.0000Costo real
summit.itsebasvz.net
Summitsito
en línea
Y ahora / la pregunta buena
¿Y en el
mundo real?
Summitsito nos sirve hoy.
La pregunta buena es qué pasa cuando
lo apuntas a problemáticas ya existentes.
EnCore
El agente del Instituto de Geofísica, UNAM. Nuestra respuesta — y lo que sigue de esta charla.
Pregúntale a Summitsito qué es ↓
Lo que acabas de construir EnCore
CloudFront ───────────────► igual
Lambda ───────────────► igual
Bedrock ───────────────► igual
Tools ───────────────► ya no leen un JSON:
llaman a la plataforma
del Instituto
DynamoDB ───────────────► Postgres, on-premise,
detrás de la red UNAM
// Cambian dos de cinco.
// El resto es exactamente el mismo dibujo.
//
// Y sirve para cualquier cosa con reglas claras:
// una ventanilla, un inventario, una agenda, un
// proceso que hoy espera la firma de alguien
// para algo que ya era evidente.
//
// Lo difícil nunca fue el modelo.
// Es decidir qué decisiones NO le dejas tomar.
//
// De ahí en adelante, el límite es la imaginación.
// La misma regla, en la plataforma del Instituto.
// Cambia el motor; no cambia quién decide.
await prisma.$transaction(async (tx) => {
const r = await verificarDisponibilidad({
tipo, recursoId, inicio, fin,
cliente: tx // DENTRO de la transacción
});
return tx.reservacionAula.create({ data: {
...datos,
estado: r.disponible ? 'APROBADA' : 'PENDIENTE'
}});
});
// El campo `estado` NO está en el esquema de
// validación. Si el cliente lo manda, se ignora
// en silencio.
//
// El agente solo puede auto-aprobar lo que un
// humano habría aprobado de todos modos, porque
// aplica el mismo predicado que aplicaría él
// mirando el calendario.
//
// Eso es lo que sobrevive a una auditoría.