initial commit: slides + practica_resueltos + README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 11:50:55 +02:00
commit 0aff31e2d3
203 changed files with 23207 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
# Curso LangChain.js — EITB
Materiales del curso de desarrollo de aplicaciones con LLMs usando **LangChain.js** y **TypeScript**.
---
## Slides
Presentación del curso.
- **Ver online**: https://nykk.io/slides-langchain-eitb/
- Carpeta: `slides/`
Arrancar en local:
```bash
cd slides && npm install && npm run dev
```
---
## Ejercicios resueltos
Código de los 6 ejercicios prácticos del curso.
- **Demo del agente (cliente web)**: https://jabiinfante.github.io/langchain.js-agent-client-dummy/
- Código fuente: https://github.com/jabiinfante/langchain.js-agent-client-dummy
- Carpeta: `practica_resueltos/`
### Ejercicios
| # | Script | Descripción |
|---|--------|-------------|
| 01 | `npm run 01:comments` | Clasificador de comentarios — Structured Output + Zod + `batch()` |
| 02 | `npm run 02:poet` | Poeta cibernético — Tools + Structured Output |
| 03 | `npm run 03:homework` | Generador de tareas — Agentic loop manual + múltiples tools |
| 04 | `npm run 04:agent` | Servidor web con agente — Fastify + SSE + memoria + RAG + generación de `.docx` |
| 05 | `npm run 05:indexer` | Indexador MDN — Web scraping + chunking + Qdrant |
| 06 | `npm run 06:indexer` | Indexador guiones de Nolan — Indexación para RAG del agente 04 |
### API Keys necesarias
| Servicio | Variable | Enlace |
|----------|----------|--------|
| Mistral AI | `MISTRAL_API_KEY` | https://console.mistral.ai/ |
| LangSmith | `LANGCHAIN_API_KEY` | https://smith.langchain.com/ |
| Cohere | `COHERE_API_KEY` | https://dashboard.cohere.com/ |
| Google AI (opcional) | `GOOGLE_API_KEY` | https://aistudio.google.com/apikey |
| Qdrant (opcional, ej. 05-06) | `QDRANT_URL` + `QDRANT_API_KEY` | https://cloud.qdrant.io/ |
Arrancar en local:
```bash
cd practica_resueltos && cp .env_sample .env # rellenar las keys
npm install && npm run 04:agent
```
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(ls:*)"
]
}
}
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/mcp-builder
+13
View File
@@ -0,0 +1,13 @@
MISTRAL_API_KEY=
GOOGLE_API_KEY=
QDRANT_URL=
QDRANT_API_KEY=
COHERE_API_KEY=
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=
LANGSMITH_API_KEY=
LANGSMITH_PROJECT="poeta"
+47
View File
@@ -0,0 +1,47 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
__screenshots__/
# System files
.DS_Store
Thumbs.db
.envoutput_docx/
.env
+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+1
View File
@@ -0,0 +1 @@
v24.16.0
+172
View File
@@ -0,0 +1,172 @@
# Ejercicios del Curso de LangChain
Este repositorio contiene los ejercicios prácticos del curso de **LangChain con TypeScript**.
## Recursos
- **Cliente del Agente (demo)**: [https://jabiinfante.github.io/langchain.js-agent-client-dummy/](https://jabiinfante.github.io/langchain.js-agent-client-dummy/)
- Código fuente: [https://github.com/jabiinfante/langchain.js-agent-client-dummy](https://github.com/jabiinfante/langchain.js-agent-client-dummy)
## Requisitos Previos
### 1. Cuentas y API Keys necesarias
#### Mistral AI (Requerido)
1. Crear cuenta en [https://console.mistral.ai/](https://console.mistral.ai/)
2. Ir a "API Keys" y generar una nueva key
3. Guardar la key como `MISTRAL_API_KEY`
#### LangSmith (Requerido para trazabilidad)
1. Crear cuenta en [https://smith.langchain.com/](https://smith.langchain.com/)
2. Ir a "Settings" → "API Keys" y crear una nueva key
3. Guardar la key como `LANGCHAIN_API_KEY`
#### Google AI / Gemini (Opcional)
1. Ir a [https://aistudio.google.com/apikey](https://aistudio.google.com/apikey)
2. Crear una API Key
3. Guardar la key como `GOOGLE_API_KEY`
#### Cohere (Requerido para reranking en el agente)
1. Crear cuenta en [https://dashboard.cohere.com/](https://dashboard.cohere.com/)
2. Ir a "API Keys" y copiar la key
3. Guardar la key como `COHERE_API_KEY`
#### Qdrant (Opcional - solo para ejercicios 05 y 06)
1. Crear cuenta en [https://cloud.qdrant.io/](https://cloud.qdrant.io/)
2. Crear un cluster gratuito
3. Obtener la URL del cluster y la API Key
4. Guardar como `QDRANT_URL` y `QDRANT_API_KEY`
### 2. Configurar variables de entorno
Crear un archivo `.env` en la raíz del proyecto:
```env
# === Mistral AI (Requerido) ===
MISTRAL_API_KEY=tu_api_key_de_mistral
# === LangSmith (Requerido para trazabilidad) ===
LANGCHAIN_API_KEY=tu_api_key_de_langsmith
LANGCHAIN_TRACING_V2=true
LANGCHAIN_PROJECT=curso-langchain
# === Google AI / Gemini (Opcional) ===
GOOGLE_API_KEY=tu_api_key_de_google
# === Cohere (Requerido para reranking) ===
COHERE_API_KEY=tu_api_key_de_cohere
# === Qdrant (Opcional - solo para ejercicios 05 y 06) ===
QDRANT_URL=https://tu-cluster.qdrant.io
QDRANT_API_KEY=tu_api_key_de_qdrant
```
### 3. Instalar dependencias
```bash
npm install
```
## Catálogo de Ejercicios
| # | Script | Archivo | Descripción |
| --- | --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 01 | `npm run 01:comments` | `01_comments_classifier.ts` | **Clasificador de Comentarios** - Analiza comentarios usando Structured Output con Zod. Demuestra `withStructuredOutput()` y `batch()` para procesar múltiples inputs. |
| 02 | `npm run 02:poet` | `02_cyber_poet.ts` | **Poeta Cibernético** - Combina Tools y Structured Output. El LLM genera un poema y usa una herramienta para contar palabras con precisión. |
| 03 | `npm run 03:homework` | `03_homework_maker.ts` | **Generador de Tareas** - Agente multi-tool con bucle agentic manual. Usa Wikipedia, Calculator y WordCount para generar tareas escolares adaptadas al nivel del alumno. |
| 04 | `npm run 04:agent` | `04_webserver_for_agent.ts` | **Servidor Web con Agente** - Integra un agente ReAct con Fastify y SSE para streaming en tiempo real. Incluye memoria persistente, RAG sobre guiones de Nolan, tasas de cambio y generación de fichas de películas en `.docx`. |
| 05 | `npm run 05:indexer` | `05_mdn-vector-indexer.ts` | **Indexador de Documentación** - Pipeline de indexación RAG que carga documentación de MDN, la divide en chunks y la almacena en Qdrant. |
| 06 | `npm run 06:indexer` | `06_nollan-indexer.ts` | **Indexador de Guiones de Nolan** - Pipeline de indexación RAG que carga guiones de películas de Christopher Nolan desde IMSDB y los almacena en Qdrant. |
## Estructura del Proyecto
```
src/
├── 01_comments_classifier.ts # Structured Output + batch
├── 02_cyber_poet.ts # Tools + Structured Output
├── 03_homework_maker.ts # Agentic Loop + PromptTemplate + múltiples tools
├── 04_webserver_for_agent.ts # Fastify + SSE + Agent con memoria
├── 05_mdn-vector-indexer.ts # Web scraping + chunking + Qdrant
├── 06_nollan-indexer.ts # Indexación de guiones de Nolan + Qdrant
├── agents_wrapper/
│ └── agent.ts # Wrapper del agente con streaming y contextSchema
└── helpers/
├── comments-mock.ts # Datos de prueba para ejercicio 01
├── constants.ts # Constantes compartidas (OUTPUT_DIR)
├── helper.ts # Utilidad promptUser() para input interactivo
├── middlewares.ts # Middleware trimMessages para limitar contexto
└── tools.ts # Herramientas: wordCount, wikipedia, exchangeRates, storageKnowledge, buildFilmDocument
template/
└── plantilla_ficha.docx # Plantilla Word para generar fichas de películas
output_docx/ # Directorio donde se generan los documentos .docx
```
## Conceptos por Ejercicio
### 01 - Clasificador de Comentarios
- **Zod**: Definición de esquemas para validación
- **withStructuredOutput()**: Forzar respuestas JSON estructuradas
- **batch()**: Procesar múltiples inputs en paralelo
### 02 - Poeta Cibernético
- **tool()**: Crear herramientas personalizadas
- **bindTools()**: Conectar herramientas al modelo
- **tool_calls**: El LLM solicita usar herramientas
- **ToolMessage**: Devolver resultados de herramientas
### 03 - Generador de Tareas
- **Agentic Loop**: Bucle while que procesa tool_calls hasta completar
- **PromptTemplate**: Plantillas con variables dinámicas
- **Múltiples Tools**: Wikipedia, Calculator, WordCount
- **promptUser()**: Input interactivo en consola
### 04 - Servidor Web con Agente
- **createAgent()**: Crear agente ReAct con herramientas
- **Checkpointer (SQLite)**: Memoria persistente de conversaciones
- **SSE (Server-Sent Events)**: Streaming de respuestas al cliente
- **contextSchema**: Inyección de dependencias a las tools
- **dynamicSystemPromptMiddleware**: System prompt que se regenera en cada invocación (incluye fecha actual)
- **QdrantVectorStore / MemoryVectorStore**: Fallback automático si Qdrant no está disponible
- **Herramientas disponibles en el agente**:
- `storage_knowledge` — RAG sobre guiones de películas de Christopher Nolan (Interstellar, Inception) con reranking via Cohere
- `get_exchange_rates` — Tasas de cambio actuales via Frankfurter API
- `get_historical_rates` — Tasas de cambio históricas de una fecha concreta
- `build_film_document` — Genera una ficha de película en `.docx` usando una plantilla Word
- `calculator` — Calculadora matemática
### 05 - Indexador de Documentación
- **CheerioWebBaseLoader**: Web scraping de HTML
- **RecursiveCharacterTextSplitter**: División de documentos en chunks
- **Embeddings**: Representación vectorial de texto
- **QdrantVectorStore**: Base de datos vectorial para búsqueda semántica
- **Deduplicación**: Eliminar vectores existentes antes de re-indexar
### 06 - Indexador de Guiones de Nolan
- **CheerioWebBaseLoader**: Web scraping de guiones desde IMSDB
- **RecursiveCharacterTextSplitter**: División en chunks con overlap
- **Batch indexing**: Inserción por lotes para respetar límites de la API de embeddings
- **CohereRerank**: Reranking de resultados (usado en el agente del ejercicio 04)
## Orden Recomendado
1. **01_comments_classifier** - Conceptos básicos de Structured Output
2. **02_cyber_poet** - Introducción a Tools
3. **03_homework_maker** - Agentic Loop manual con múltiples tools
4. **06_nollan-indexer** - Indexar guiones de Nolan en Qdrant (necesario para el agente)
5. **04_webserver_for_agent** - Agente completo con servidor web, streaming y generación de documentos
6. **05_mdn-vector-indexer** - Indexación de documentación MDN para RAG (opcional)
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "langchain_ejercicios",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"01:comments": "tsx --env-file=.env --watch src/01_comments_classifier.ts",
"02:poet": "tsx --env-file=.env --watch src/02_cyber_poet.ts",
"03:homework": "tsx --env-file=.env src/03_homework_maker.ts",
"04:agent": "tsx --env-file=.env --watch src/04_webserver_for_agent.ts",
"05:indexer": "tsx --env-file=.env src/05_mdn-vector-indexer.ts",
"06:indexer": "tsx --env-file=.env src/06_nollan-indexer.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@fastify/cors": "^11.2.0",
"@langchain/cohere": "^1.0.4",
"@langchain/community": "^1.1.4",
"@langchain/core": "^1.1.15",
"@langchain/google": "^0.1.7",
"@langchain/langgraph-checkpoint-sqlite": "^1.0.0",
"@langchain/mistralai": "^1.0.2",
"@langchain/qdrant": "^1.0.3",
"@langchain/textsplitters": "^1.0.1",
"@qdrant/js-client-rest": "^1.18.0",
"cheerio": "^1.1.2",
"docxtemplater": "^3.68.7",
"fastify": "^5.7.1",
"fastify-sse-v2": "^4.2.1",
"langchain": "^1.2.10",
"pizzip": "^3.2.0",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/node": "^25.0.9",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
}
}
@@ -0,0 +1,98 @@
/**
* =============================================================================
* EJERCICIO: Clasificador de Comentarios con Structured Output
* =============================================================================
*
* Este ejercicio demuestra cómo usar LangChain para clasificar comentarios
* de usuarios utilizando "Structured Output" (salida estructurada).
*
* CONCEPTOS CLAVE:
* - Zod: Librería para definir y validar esquemas de datos en TypeScript
* - withStructuredOutput(): Método que fuerza al LLM a responder con un JSON
* que cumple exactamente con el esquema definido
* - batch(): Procesar múltiples inputs en paralelo de forma eficiente
*
* FLUJO DEL EJERCICIO:
* 1. Definir un esquema con Zod que describe la estructura de respuesta esperada
* 2. Configurar el modelo LLM con withStructuredOutput()
* 3. Enviar comentarios al modelo para que los analice y clasifique
* 4. Recibir respuestas estructuradas (JSON válido según el esquema)
* =============================================================================
*/
import { z } from "zod";
import { ChatMistralAI } from "@langchain/mistralai";
import { initChatModel } from "langchain";
import { comments } from "./helpers/comments-mock";
// =============================================================================
// PASO 1: Definir el esquema de respuesta con Zod
// =============================================================================
// Zod nos permite definir la estructura exacta que esperamos del LLM.
// Esto garantiza que la respuesta sea un JSON válido y tipado.
const CommentSchema = z.object({
// Nivel de lenguaje ofensivo (0 = ninguno, 5 = muy ofensivo)
profanity_level: z.number().min(0).max(5),
// Sentimiento general del comentario
sentiment: z.enum(["positive", "neutral", "negative"]),
// Temas identificados en el comentario (mínimo 1)
topics: z.array(z.string()).min(1)
}).describe("Esquema para analizar comentarios de usuarios");
// TypeScript infiere automáticamente el tipo desde el esquema Zod
type Comment = z.infer<typeof CommentSchema>;
// =============================================================================
// PASO 2: Ejemplo de validación con Zod (sin LLM)
// =============================================================================
// Podemos usar el esquema para validar datos manualmente.
// Si los datos no cumplen el esquema, Zod lanzará un error.
const result = CommentSchema.parse({
profanity_level: 5,
sentiment: "neutral",
topics: ["technology", "education"]
});
// =============================================================================
// PASO 3: Configurar el modelo LLM con Structured Output
// =============================================================================
// withStructuredOutput() recibe el esquema Zod y configura el modelo para
// que SIEMPRE responda con un JSON que cumpla ese esquema.
// Opción A: Usar Mistral AI directamente
const llm = new ChatMistralAI({
model: "mistral-large-latest",
streaming: true,
maxTokens: 1000,
}).withStructuredOutput(CommentSchema);
// Opción B: Usar initChatModel() para inicializar cualquier modelo de forma genérica
// Esto permite cambiar fácilmente entre proveedores (OpenAI, Google, Anthropic, etc.)
const llm2 = (await initChatModel('gemini-2.5-flash', {
modelProvider: 'google'
})).withStructuredOutput(CommentSchema);
// =============================================================================
// PASO 4: Procesar comentarios en lote (batch)
// =============================================================================
// batch() permite enviar múltiples inputs al modelo de forma eficiente.
// maxConcurrency limita cuántas peticiones se hacen en paralelo.
const result2 = await llm.batch(
comments.map(c => c.content), // Extraer solo el contenido de cada comentario
{ maxConcurrency: 2 } // Máximo 2 peticiones simultáneas
);
// =============================================================================
// PASO 5: Mostrar resultados
// =============================================================================
// Cada resultado es un objeto tipado que cumple con CommentSchema
result2.forEach(async (result, index) => {
console.log(`Respuesta a la pregunta ${comments[index].uuid} (${comments[index].content}):`);
console.dir(result, { depth: null, colors: true });
});
+181
View File
@@ -0,0 +1,181 @@
/**
* =============================================================================
* EJERCICIO: Poeta Cibernético con Tools y Structured Output
* =============================================================================
*
* Este ejercicio combina dos conceptos importantes de LangChain:
* - Tools (herramientas): Funciones que el LLM puede invocar para realizar tareas
* - Structured Output: Forzar al LLM a responder con un formato JSON específico
*
* ENUNCIADO INICIAL (problema a resolver):
* ─────────────────────────────────────────────────────────────────────────────
* import { ChatMistralAI } from "@langchain/mistralai";
*
* const llm = new ChatMistralAI({
* model: "mistral-large-latest",
* temperature: 0.2,
* });
*
* const prompt = `Eres un poeta cybernetico. Respondes en castellano.
* Tu estilo es oscuro y meláncolico.
* Necesito que además del poema me devuelvas 3 palabras clave del poema así
* como el número exacto de palabras del texto que generes.`;
*
* const response = await llm.invoke(prompt);
* console.dir(response, { depth: null, colors: true });
* ─────────────────────────────────────────────────────────────────────────────
*
* PROBLEMA: El LLM no puede contar palabras con precisión (alucinará el número).
* SOLUCIÓN: Usar una Tool que cuente las palabras de forma precisa.
*
* CONCEPTOS CLAVE:
* - tool(): Función de LangChain para crear herramientas que el LLM puede usar
* - bindTools(): Conectar herramientas a un modelo LLM
* - tool_calls: El LLM indica qué herramienta quiere usar y con qué argumentos
* - ToolMessage: Mensaje que contiene el resultado de ejecutar una herramienta
* - withStructuredOutput(): Forzar formato de respuesta final
*
* FLUJO DEL EJERCICIO:
* 1. Definir una herramienta para contar palabras
* 2. El LLM genera un poema y solicita contar sus palabras (tool_call)
* 3. Ejecutamos la herramienta y devolvemos el resultado (ToolMessage)
* 4. El LLM genera la respuesta final estructurada con el conteo correcto
* =============================================================================
*/
import {
AIMessage,
HumanMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { ChatMistralAI } from "@langchain/mistralai";
// =============================================================================
// PASO 1: Definir la herramienta (Tool) para contar palabras
// =============================================================================
// Las Tools son funciones que el LLM puede decidir invocar.
// El LLM NO ejecuta la función directamente, solo indica que quiere usarla.
// Nosotros ejecutamos la función y le devolvemos el resultado.
const wordCountTool = tool(
({ texto }) => {
// Esta función se ejecuta cuando procesamos el tool_call del LLM
process.stdout.write("Contando palabras...");
const words = texto.trim().split(/\s+/).filter(Boolean).length;
process.stdout.write(` [${words}]\n`);
return words.toString();
},
{
name: "contar_palabras",
description:
"Cuenta el numero de palabras en un texto, y devuelve solo el número.",
// Zod define qué parámetros acepta la herramienta
schema: z.object({ texto: z.string() }),
},
);
// =============================================================================
// PASO 2: Configurar el modelo con las herramientas
// =============================================================================
// bindTools() conecta las herramientas al modelo.
// Ahora el LLM sabe que puede usar "contar_palabras" cuando lo necesite.
const llm = new ChatMistralAI({
model: "mistral-large-latest",
temperature: 0,
});
// IMPORTANTE: Pasar el array con las herramientas disponibles
const modelWithTools = llm.bindTools([wordCountTool]);
// =============================================================================
// PASO 3: Preparar la conversación inicial
// =============================================================================
// Usamos diferentes tipos de mensajes:
// - SystemMessage: Define el comportamiento/personalidad del LLM
// - HumanMessage: El mensaje del usuario
// - AIMessage: Respuesta del LLM (se añade después)
// - ToolMessage: Resultado de ejecutar una herramienta
const messages: Array<SystemMessage | AIMessage | HumanMessage | ToolMessage> =
[
new SystemMessage(
"Eres un poeta cybernetico. Respondes en castellano. Tu estilo es oscuro y meláncolico.",
),
new HumanMessage(
`Necesito que generes un unico poema. Que uses las herramientas disponibles para contar las palabras de **ese** poema.
`,
),
];
// =============================================================================
// PASO 4: Primera invocación - El LLM genera el poema y pide contar palabras
// =============================================================================
// El LLM responderá con:
// - content: El poema generado
// - tool_calls: Array indicando que quiere usar "contar_palabras"
const firstResponse = await modelWithTools.invoke(messages);
// Añadimos la respuesta del LLM al historial de mensajes
messages.push(firstResponse as AIMessage);
// =============================================================================
// PASO 5: Procesar los tool_calls (ejecutar las herramientas solicitadas)
// =============================================================================
// Si el LLM quiere usar herramientas, las ejecutamos y añadimos el resultado.
if (firstResponse.tool_calls && firstResponse.tool_calls.length > 0) {
for (const call of firstResponse.tool_calls) {
// Ejecutar la herramienta correspondiente
let toolResult: string;
if (call.name === "contar_palabras") {
console.log("LLM solicitó contar palabras del poema...");
toolResult = await wordCountTool.invoke(call.args as { texto: string });
} else {
throw new Error(`Herramienta desconocida: ${call.name}`);
}
// Añadir el resultado como ToolMessage al historial
// IMPORTANTE: tool_call_id debe coincidir con el id del tool_call
console.log(`Devolviendo resultado de herramienta: ${toolResult}`);
messages.push(
new ToolMessage({
content:
typeof toolResult === "string"
? toolResult
: JSON.stringify(toolResult),
tool_call_id: call.id!,
}),
);
}
// ===========================================================================
// PASO 6: Generar la respuesta final estructurada
// ===========================================================================
// Ahora el LLM tiene el conteo real de palabras en el historial.
// Usamos withStructuredOutput() para obtener un JSON con formato específico.
const OutputSchema = z.object({
poema: z
.array(z.string())
.describe(
"El poema generado por el poeta cybernetico. Cada verso en una línea separada.",
),
tematica: z.array(z.string()).length(3).describe("3 palabras clave del poema"),
total_palabras: z.number().describe("El número exacto de palabras del poema"),
});
const modelWithOutput = llm.withStructuredOutput(OutputSchema);
console.log("Generando salida final con conteo de palabras...");
const final = await modelWithOutput.invoke(messages);
console.dir(messages, { depth: null, colors: true });
console.dir(final, { depth: null, colors: true });
} else {
// Si el LLM no pidió usar herramientas, mostramos su respuesta directa
console.log(firstResponse.content);
}
+250
View File
@@ -0,0 +1,250 @@
/**
* =============================================================================
* EJERCICIO: Generador de Tareas Escolares con Agente Multi-Tool
* =============================================================================
*
* Este ejercicio demuestra un caso de uso avanzado combinando múltiples conceptos:
* - Bucle de agente manual (agentic loop): El modelo decide cuándo usar herramientas
* - PromptTemplate: Plantillas de prompts con variables dinámicas
* - Múltiples herramientas: Wikipedia, Calculator, Word Counter
* - Structured Output: Respuesta final en formato JSON estructurado
* - Input interactivo: Configuración mediante prompts de usuario
*
* CASO DE USO:
* Un estudiante necesita ayuda con sus deberes. El agente:
* 1. Recibe la configuración (nivel, asignatura, idioma, extensión)
* 2. Busca información relevante en Wikipedia
* 3. Genera el texto adaptado al nivel del alumno
* 4. Verifica que cumple con la extensión requerida
* 5. Devuelve un informe estructurado
*
* CONCEPTOS CLAVE:
* - PromptTemplate.fromTemplate(): Crear prompts con variables {variable}
* - bindTools(): Conectar múltiples herramientas al modelo
* - Agentic Loop: Bucle while que procesa tool_calls hasta completar la tarea
* - withStructuredOutput(): Generar respuesta final estructurada
*
* FLUJO DEL EJERCICIO:
* 1. Recoger configuración del usuario (nivel, asignatura, idioma, etc.)
* 2. Formatear el system prompt con las variables
* 3. Ejecutar bucle de agente:
* - Invocar modelo
* - Si hay tool_calls: ejecutar herramientas y añadir resultados
* - Repetir hasta que no haya más tool_calls o se alcance el límite
* 4. Generar informe final estructurado
* =============================================================================
*/
import { Calculator } from "@langchain/community/tools/calculator";
import { WikipediaQueryRun } from "@langchain/community/tools/wikipedia_query_run";
import {
BaseMessage,
HumanMessage,
SystemMessage,
ToolMessage
} from "@langchain/core/messages";
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatMistralAI } from "@langchain/mistralai";
import { z } from "zod";
import { promptUser } from "./helpers/helper";
import { wordCountTool } from "./helpers/tools";
// =============================================================================
// PASO 1: Recoger configuración del usuario
// =============================================================================
// promptUser() es un helper que muestra un prompt en consola y recoge la respuesta.
// El segundo parámetro es el valor por defecto si el usuario presiona Enter.
const config = {
level: await promptUser("¿En qué curso está el alumno?", "Primero de la ESO"),
subject: await promptUser("¿Cuál es la asignatura de la tarea?", "Historia"),
language: await promptUser("¿En qué idioma está la tarea?", "Euskera"),
number_words: await promptUser(
"¿Cuántas palabras debe tener la tarea?",
"500",
),
question: await promptUser(
"Introduce la pregunta que quieres que responda el alumno:",
"Pequeña redacción con la historia del Imperio Romano",
),
};
// =============================================================================
// PASO 2: Configurar el modelo y las herramientas
// =============================================================================
const model = new ChatMistralAI({
model: "mistral-large-latest",
temperature: 0.2, // Baja temperatura para respuestas más consistentes
});
// Wikipedia: Buscar información de referencia
const wikipediaTool = new WikipediaQueryRun({
topKResults: 3, // Máximo 3 resultados
maxDocContentLength: 1500, // Limitar contenido para no exceder contexto
});
// Calculator: Para cálculos matemáticos si la tarea lo requiere
const calculatorTool = new Calculator();
// Conectar todas las herramientas al modelo
const modelWithTools = model.bindTools([
wordCountTool, // Contar palabras (verificar extensión)
wikipediaTool, // Buscar información
calculatorTool, // Cálculos matemáticos
]);
// =============================================================================
// PASO 3: Crear el prompt template con variables
// =============================================================================
// PromptTemplate permite crear prompts reutilizables con placeholders {variable}
// que se sustituyen al llamar a format()
const systemPromptTemplate = PromptTemplate.fromTemplate(`
Eres un asistente educativo experto que ayuda a estudiantes a completar sus tareas escolares.
Tu objetivo es generar contenido educativo de alta calidad adaptado al nivel del alumno.
## CONTEXTO DEL ALUMNO
- **Nivel educativo:** {level}
- **Asignatura:** {subject}
- **Idioma de la tarea:** {language}
- **Extensión requerida:** {number_words} palabras (margen: ±10%)
## HERRAMIENTAS DISPONIBLES
1. **wikipedia-api** - Búsqueda de información
- Usar para obtener datos precisos y verificables
- Buscar en español para obtener mejores resultados
- Máximo 2 búsquedas en Wikipedia para no sobrecargar el servicio
2. **calculator** - Calculadora matemática
- USAR para cualquier cálculo numérico (fechas, porcentajes, estadísticas)
3. **contar_palabras** - Verificación de extensión
- USAR OBLIGATORIAMENTE antes de dar la respuesta final
- Si el conteo está fuera del rango permitido, ajustar el texto
## FLUJO DE TRABAJO
1. **INVESTIGAR**: Busca información en Wikipedia sobre el tema solicitado
2. **PLANIFICAR**: Organiza las ideas principales según el nivel educativo
3. **REDACTAR**: Escribe el texto en {language}, adaptando vocabulario y complejidad
4. **VERIFICAR**: Cuenta las palabras y ajusta si es necesario
5. **ENTREGAR**: Proporciona el texto final verificado
## REGLAS DE CALIDAD
- **Adaptación al nivel**: Un alumno de primaria necesita lenguaje simple; uno de bachillerato puede manejar conceptos más complejos
- **Estructura clara**: Usa párrafos bien organizados con introducción, desarrollo y conclusión
- **Precisión**: Todos los datos deben provenir de Wikipedia, no inventes información
- **Originalidad**: Redacta con tus propias palabras, no copies textualmente de Wikipedia
- **Idioma**: TODO el contenido debe estar en {language}, incluyendo términos técnicos cuando sea posible
## IMPORTANTE
- NO entregues la tarea sin verificar el conteo de palabras
- Si el texto es muy corto, amplía con más detalles o ejemplos
- Si el texto es muy largo, sintetiza manteniendo la información esencial
`);
// =============================================================================
// PASO 4: Preparar mensajes iniciales
// =============================================================================
const { question, ...params } = config;
// Formatear el prompt sustituyendo las variables
const systemPrompt = await systemPromptTemplate.format(params);
const messages: BaseMessage[] = [
new SystemMessage(systemPrompt),
new HumanMessage(question),
];
// =============================================================================
// PASO 5: Bucle de agente (Agentic Loop)
// =============================================================================
// Este bucle implementa el patrón ReAct manualmente:
// - El modelo genera una respuesta (puede incluir tool_calls)
// - Si hay tool_calls, ejecutamos las herramientas y añadimos los resultados
// - Repetimos hasta que el modelo no pida más herramientas
let iteracion = 0;
const MAX_ITERACIONES = 6; // Límite de seguridad para evitar bucles infinitos
while (iteracion < MAX_ITERACIONES) {
iteracion++;
console.log(`\n🔄 Iteración ${iteracion}...`);
// Invocar el modelo con el historial de mensajes
const response = await modelWithTools.invoke(messages);
messages.push(response);
// Si no hay tool_calls, el modelo ha terminado
if (!response.tool_calls || response.tool_calls.length === 0) {
console.log(`\n✅ Tarea completada`);
break;
}
// Procesar cada tool_call solicitado por el modelo
for (const call of response.tool_calls) {
console.log(`\n🔧 Tool: ${call.name}`);
console.log(` 📝 Args: ${JSON.stringify(call.args)}`);
let resultado: string;
// Ejecutar la herramienta correspondiente
if (call.name === "contar_palabras") {
resultado = `${await wordCountTool.invoke(call.args as { texto: string })}`;
} else if (call.name === "wikipedia-api") {
console.log(` 🌐 Buscando en Wikipedia: "${call.args.input}"`);
await new Promise((r) => setTimeout(r, 1500));
const wikiResults = await wikipediaTool.invoke(call.args.input as string);
resultado =
typeof wikiResults === "string"
? wikiResults
: JSON.stringify(wikiResults);
} else if (call.name === "calculator") {
console.log(` 🧮 Calculando: "${call.args.input}"`);
resultado = `${await calculatorTool.invoke(call.args.input as string)}`;
} else {
resultado = `Error: herramienta "${call.name}" no reconocida`;
}
// Añadir el resultado de la herramienta como ToolMessage
messages.push(
new ToolMessage({
content: resultado,
tool_call_id: call.id!,
}),
);
}
}
// =============================================================================
// PASO 6: Generar informe final estructurado
// =============================================================================
// Una vez completada la tarea, pedimos un informe en formato JSON.
const homeworkReportSchema = z.object({
texto: z.string().describe("El texto completo de la tarea para el alumno"),
numero_palabras: z.number().describe("Número total de palabras en el texto"),
nivel: z.string().describe("Nivel educativo del alumno"),
urls_wikipedia: z
.array(z.string())
.describe("URLs de Wikipedia consultadas (si las hay)"),
});
console.log(`\n📝 Generando informehomeworkReportSchema final...`);
messages.push(
new HumanMessage(
"Genera el informe final con la tarea completada para el alumno.",
),
);
const modelWithOutput = model.withStructuredOutput(homeworkReportSchema);
const informe = await modelWithOutput.invoke(messages);
console.log("\n" + "=".repeat(60));
console.log("INFORME FINAL");
console.log("=".repeat(60));
console.dir(informe, { depth: null, colors: true });
@@ -0,0 +1,213 @@
/**
* =============================================================================
* EJERCICIO: Servidor Web con Agente LangChain y Streaming (SSE)
* =============================================================================
*
* Este ejercicio demuestra cómo integrar un agente de LangChain con un servidor
* web Fastify, usando Server-Sent Events (SSE) para streaming en tiempo real.
*
* ARQUITECTURA:
* ┌─────────────┐ POST /message ┌─────────────┐
* │ Cliente │ ─────────────────────▶│ Fastify │
* │ (Browser) │ │ Server │
* │ │◀───────────────────── │ │
* └─────────────┘ SSE /stream └──────┬──────┘
* │
* ▼
* ┌─────────────┐
* │ Agent │
* │ (LangChain) │
* └─────────────┘
*
* CONCEPTOS CLAVE:
* - SSE (Server-Sent Events): Protocolo para enviar datos del servidor al cliente
* en tiempo real, ideal para streaming de respuestas de LLMs
* - Checkpointer: Guarda el estado de la conversación (memoria persistente)
* - thread_id: Identificador único de conversación para mantener contexto
*
* FLUJO:
* 1. Cliente abre conexión SSE en GET /stream (recibe mensajes en tiempo real)
* 2. Cliente envía mensaje POST /message
* 3. Servidor pasa mensaje al Agente
* 4. Agente procesa y genera respuesta (puede usar tools)
* 5. Cada chunk de respuesta se envía al cliente vía SSE
*
* DEPENDENCIAS:
* - fastify: Framework web rápido y de bajo consumo para Node.js
* - fastify-sse-v2: Plugin para manejar Server-Sent Events
* - @fastify/cors: Plugin para habilitar CORS
* =============================================================================
*/
import cors from "@fastify/cors";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";
import { MistralAIEmbeddings } from "@langchain/mistralai";
import { QdrantVectorStore } from "@langchain/qdrant";
import Fastify from "fastify";
import { FastifySSEPlugin } from "fastify-sse-v2";
import { initChatModel } from "langchain";
import { createReadStream, readdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { Agent } from "./agents_wrapper/agent";
import { OUTPUT_DIR } from "./helpers/constants";
// =============================================================================
// PASO 1: Inicializar el Agente con memoria persistente
// =============================================================================
// El agente se inicializa ANTES del servidor para que esté listo cuando
// lleguen las peticiones. SqliteSaver guarda el historial de conversaciones.
const model = await initChatModel("mistral:mistral-large-latest", {
timeout: 60000, // 1 minuto de timeout para respuestas largas
});
const embeddings = new MistralAIEmbeddings({
model: "mistral-embed",
});
// VectorStore: Usamos Qdrant para almacenamiento persistente de vectores
// fallback a MemoryVectorStore si Qdrant no está disponible (desarrollo local)
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, {
url: process.env.QDRANT_URL,
collectionName: "nolan-scripts",
apiKey: process.env.QDRANT_API_KEY,
}).catch(() => new MemoryVectorStore(embeddings));
// Checkpointer: Guarda el estado de las conversaciones en SQLite
// Permite que las conversaciones persistan entre reinicios del servidor
const checkpointer = SqliteSaver.fromConnString(
join(tmpdir(), "agent_memory.db"),
);
const agent = new Agent(model, vectorStore, checkpointer);
// =============================================================================
// PASO 2: Configurar el servidor Fastify
// =============================================================================
const fastify = Fastify({
logger: true, // Habilitar logs para debugging
});
// CORS: Permitir peticiones desde cualquier origen (desarrollo)
// En producción, restringir a dominios específicos
fastify.register(cors, {
origin: "*",
});
// Plugin SSE: Habilita el método reply.sse() para streaming
fastify.register(FastifySSEPlugin);
// =============================================================================
// PASO 3: Ruta de health check
// =============================================================================
fastify.get("/", async (request, reply) => {
return { message: "Hola agente!", status: "running" };
});
// =============================================================================
// PASO 4: Ruta POST /message - Recibir mensajes del cliente
// =============================================================================
// El cliente envía un mensaje y el servidor lo pasa al agente.
// La respuesta se envía de forma asíncrona vía SSE (no en esta ruta).
fastify.post<{ Querystring: { uuid?: string }; Body: { message: string } }>(
"/message",
async (request, reply) => {
const { message } = request.body;
// thread_id identifica la conversación
// En producción: obtener de cookies, headers, JWT, etc.
const thread_id = request.query.uuid || "chat-id-XXX";
// Enviar mensaje al agente (procesamiento asíncrono)
// Las respuestas se enviarán vía SSE a los clientes suscritos
agent.messageReceived(message, { thread_id });
// Respuesta inmediata: confirmar que el mensaje fue recibido
return { status: "Mensaje recibido", thread_id };
},
);
// =============================================================================
// PASO 5: Ruta GET /stream - Conexión SSE para recibir respuestas
// =============================================================================
// El cliente mantiene una conexión abierta y recibe mensajes en tiempo real.
// Documentación SSE: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
fastify.get<{ Querystring: { uuid?: string } }>(
"/stream",
async (request, reply) => {
const thread_id = request.query.uuid || "chat-id-XXX";
// Enviar evento de conexión establecida
reply.sse({
data: JSON.stringify({ connected: true }),
event: "connected",
});
// Handler: Se ejecuta cada vez que el agente genera un nuevo mensaje
const handler = (message: any) => {
reply.sse({ data: JSON.stringify(message), event: "message" });
};
// Registrar el handler para esta conversación (thread_id)
agent.registerNewMessageHandler(handler, thread_id);
// Cleanup: Desregistrar cuando el cliente cierra la conexión
reply.raw.on("close", () => {
request.log.info(`SSE connection closed for thread: ${thread_id}`);
agent.unregisterNewMessageHandler(thread_id, handler);
});
},
);
fastify.get("/files", async (_request, reply) => {
const files = readdirSync(OUTPUT_DIR).map((name) => ({
name,
url: `/files/${encodeURIComponent(name)}`,
}));
return reply.send(files);
});
fastify.get<{ Params: { filename: string } }>(
"/files/:filename",
async (request, reply) => {
const filename = decodeURIComponent(request.params.filename);
const safePath = resolve(OUTPUT_DIR, filename);
if (!safePath.startsWith(OUTPUT_DIR)) {
return reply.status(403).send({ error: "Forbidden" });
}
reply.header(
"Content-Disposition",
`attachment; filename="${encodeURIComponent(filename)}"`,
);
reply.header(
"Content-Type",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
);
return reply.send(createReadStream(safePath));
},
);
// =============================================================================
// PASO 6: Arrancar el servidor
// =============================================================================
try {
await fastify.listen({ port: 3000, host: "0.0.0.0" });
console.log("Servidor corriendo en http://localhost:3000");
console.log("Endpoints disponibles:");
console.log(" GET / - Health check");
console.log(" POST /message - Enviar mensaje al agente");
console.log(" GET /stream - Conexión SSE para recibir respuestas");
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
@@ -0,0 +1,193 @@
/**
* =============================================================================
* EJERCICIO: Indexador de Documentación MDN en Vector Store (Qdrant)
* =============================================================================
*
* Este script demuestra cómo crear un pipeline de indexación para RAG:
* 1. Cargar documentos desde URLs (web scraping)
* 2. Dividir documentos en chunks más pequeños
* 3. Generar embeddings y almacenarlos en un vector store
*
* CONCEPTOS CLAVE:
* - CheerioWebBaseLoader: Carga contenido HTML de URLs y extrae texto
* - RecursiveCharacterTextSplitter: Divide documentos en chunks con overlap
* - Embeddings: Representación vectorial del texto para búsqueda semántica
* - Vector Store (Qdrant): Base de datos optimizada para búsqueda por similitud
*
* FLUJO DEL EJERCICIO:
* 1. Definir URLs de MDN a indexar
* 2. Cargar el contenido HTML de cada URL
* 3. Limpiar y dividir en chunks
* 4. **Eliminar vectores existentes** para evitar duplicados
* 5. Generar embeddings e insertar en Qdrant
* 6. Verificar con una búsqueda de prueba
*
* NOTA SOBRE DUPLICADOS:
* Este script elimina los vectores existentes que coincidan con las URLs
* a indexar antes de insertar los nuevos. Esto evita duplicados pero
* re-indexa siempre el contenido.
*
* TODO: Lo ideal sería comprobar si el contenido ha cambiado (ej: hash del
* contenido o fecha de modificación) antes de re-indexar, para evitar
* trabajo innecesario. Por simplicidad, no lo implementamos aquí.
* =============================================================================
*/
import { MistralAIEmbeddings } from "@langchain/mistralai";
import { QdrantVectorStore } from "@langchain/qdrant";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { QdrantClient } from "@qdrant/js-client-rest";
// =============================================================================
// PASO 1: Configuración
// =============================================================================
const COLLECTION_NAME = "langchainjs-testing-dia2";
// URLs de MDN a indexar (documentación sobre Web Storage)
const urls = [
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API",
"https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API",
// Descomentar para indexar más documentación:
// "https://developer.mozilla.org/en-US/docs/Web/API/Storage",
// "https://developer.mozilla.org/en-US/docs/Web/API/Storage_API",
// "https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria",
// "https://developer.mozilla.org/en-US/docs/Web/API/StorageManager",
// "https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API",
// "https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API/Using",
// "https://developer.mozilla.org/en-US/docs/Web/API/Shared_Storage_API",
];
// Configurar embeddings de Mistral
const embeddings = new MistralAIEmbeddings({
model: "mistral-embed",
});
// =============================================================================
// PASO 2: Cargar documentos desde las URLs
// =============================================================================
// CheerioWebBaseLoader usa Cheerio (parser HTML) para extraer contenido.
// El selector "main#content" extrae solo el contenido principal de MDN.
console.log("📥 Cargando documentos desde MDN...\n");
const loaders = urls.map(
(url) => new CheerioWebBaseLoader(url, { selector: "main#content" }),
);
const docs = [];
for (const loader of loaders) {
console.log(`${loader.webPath}`);
const loadedDocs = await loader.load();
docs.push(...loadedDocs);
}
console.log(`\n✅ ${docs.length} documentos cargados\n`);
// =============================================================================
// PASO 3: Limpiar y dividir documentos en chunks
// =============================================================================
// - Limpiamos saltos de línea excesivos para reducir ruido
// - Dividimos en chunks de ~1000 caracteres con 150 de overlap
// - El overlap ayuda a mantener contexto entre chunks adyacentes
// Limpiar pageContent: reemplazar múltiples saltos de línea por uno solo
docs.forEach((doc) => {
doc.pageContent = doc.pageContent.replace(/\n{2,}/g, "\n");
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000, // Tamaño máximo de cada chunk en caracteres
chunkOverlap: 150, // Solapamiento entre chunks consecutivos
});
const allSplits = await splitter.splitDocuments(docs);
console.log(`📄 Documentos divididos en ${allSplits.length} chunks\n`);
// =============================================================================
// PASO 4: Eliminar vectores existentes para evitar duplicados
// =============================================================================
// Antes de insertar, eliminamos los vectores que ya existen para las URLs
// que vamos a indexar. Usamos el metadato "source" que contiene la URL.
//
// NOTA: Lo ideal sería verificar si el contenido ha cambiado antes de
// re-indexar (usando un hash o fecha de modificación), pero por simplicidad
// siempre eliminamos y re-insertamos.
console.log("🗑️ Eliminando vectores existentes para evitar duplicados...\n");
const qdrantClient = new QdrantClient({
url: process.env.QDRANT_URL,
apiKey: process.env.QDRANT_API_KEY,
});
// Eliminar vectores por cada URL (filtrando por el metadato "source")
for (const url of urls) {
try {
await qdrantClient.delete(COLLECTION_NAME, {
filter: {
must: [
{
key: "metadata.source",
match: { value: url },
},
],
},
});
console.log(` → Eliminados vectores de: ${url}`);
} catch (error: any) {
// Si la colección no existe, no hay nada que eliminar
if (error.status === 404 || error.message?.includes("not found")) {
console.log(` → Colección no existe aún, se creará al insertar`);
break; // No hace falta seguir intentando eliminar
}
throw error;
}
}
console.log("");
// =============================================================================
// PASO 5: Generar embeddings e insertar en Qdrant
// =============================================================================
// QdrantVectorStore.fromDocuments():
// - Genera embeddings para cada chunk
// - Los inserta en la colección de Qdrant
// - Crea la colección si no existe
console.log("📤 Generando embeddings e insertando en Qdrant...\n");
const vectorStore = await QdrantVectorStore.fromDocuments(
allSplits,
embeddings,
{
url: process.env.QDRANT_URL,
collectionName: COLLECTION_NAME,
apiKey: process.env.QDRANT_API_KEY,
},
);
console.log(`${allSplits.length} vectores insertados en "${COLLECTION_NAME}"\n`);
// =============================================================================
// PASO 6: Verificar con una búsqueda de prueba
// =============================================================================
// Hacemos una búsqueda semántica para verificar que todo funciona.
console.log("🔍 Verificando con búsqueda de prueba...\n");
const query = "¿Qué capacidad máxima en megas puedo usar para localStorage en un navegador?";
console.log(`Query: "${query}"\n`);
const retrievedDocs = await vectorStore.similaritySearch(query, 2);
console.log("Documentos recuperados:");
retrievedDocs.forEach((doc, idx) => {
console.log(`\n--- Documento ${idx + 1} ---`);
console.log(`Fuente: ${doc.metadata.source}`);
console.log(`Contenido (primeros 300 chars):`);
console.log(doc.pageContent.substring(0, 300) + "...");
});
console.log("\n✅ Indexación completada");
+191
View File
@@ -0,0 +1,191 @@
/**
* =============================================================================
* EJERCICIO: Indexador de Guiones de Christopher Nolan en Vector Store (Qdrant)
* =============================================================================
*
* Este script demuestra cómo crear un pipeline de indexación para RAG:
* 1. Cargar guiones de películas desde URLs (web scraping de IMSDB)
* 2. Dividir documentos en chunks más pequeños
* 3. Generar embeddings y almacenarlos en un vector store
*
* CONCEPTOS CLAVE:
* - CheerioWebBaseLoader: Carga contenido HTML de URLs y extrae texto
* - RecursiveCharacterTextSplitter: Divide documentos en chunks con overlap
* - Embeddings: Representación vectorial del texto para búsqueda semántica
* - Vector Store (Qdrant): Base de datos optimizada para búsqueda por similitud
*
* FLUJO DEL EJERCICIO:
* 1. Definir URLs de guiones de Nolan en IMSDB
* 2. Cargar el contenido HTML de cada URL (selector: td.scrtext)
* 3. Limpiar y dividir en chunks
* 4. **Eliminar vectores existentes** para evitar duplicados
* 5. Generar embeddings e insertar en Qdrant
* 6. Verificar con una búsqueda de prueba
*
* NOTA SOBRE DUPLICADOS:
* Este script elimina los vectores existentes que coincidan con las URLs
* a indexar antes de insertar los nuevos. Esto evita duplicados pero
* re-indexa siempre el contenido.
*
* TODO: Lo ideal sería comprobar si el contenido ha cambiado (ej: hash del
* contenido o fecha de modificación) antes de re-indexar, para evitar
* trabajo innecesario. Por simplicidad, no lo implementamos aquí.
* =============================================================================
*/
import { MistralAIEmbeddings } from "@langchain/mistralai";
import { QdrantVectorStore } from "@langchain/qdrant";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { QdrantClient } from "@qdrant/js-client-rest";
// =============================================================================
// PASO 1: Configuración
// =============================================================================
const COLLECTION_NAME = "nolan-scripts";
// URLs de guiones de películas de Christopher Nolan en IMSDB
const urls = [
"https://imsdb.com/scripts/Interstellar.html",
"https://imsdb.com/scripts/Inception.html",
];
// Configurar embeddings de Mistral
const embeddings = new MistralAIEmbeddings({
model: "mistral-embed",
});
// =============================================================================
// PASO 2: Cargar documentos desde las URLs
// =============================================================================
// CheerioWebBaseLoader usa Cheerio (parser HTML) para extraer contenido.
// El selector "td.scrtext" extrae el texto del guión en IMSDB.
console.log("📥 Cargando guiones de Christopher Nolan desde IMSDB...\n");
const loaders = urls.map(
(url) => new CheerioWebBaseLoader(url, { selector: "td.scrtext" }),
);
const docs = [];
for (const loader of loaders) {
console.log(`${loader.webPath}`);
const loadedDocs = await loader.load();
docs.push(...loadedDocs);
}
console.log(`\n✅ ${docs.length} documentos cargados\n`);
// =============================================================================
// PASO 3: Limpiar y dividir documentos en chunks
// =============================================================================
// - Limpiamos saltos de línea excesivos para reducir ruido
// - Dividimos en chunks de ~1000 caracteres con 150 de overlap
// - El overlap ayuda a mantener contexto entre chunks adyacentes
// Limpiar pageContent: reemplazar múltiples saltos de línea por uno solo
docs.forEach((doc) => {
doc.pageContent = doc.pageContent.replace(/\n{2,}/g, "\n");
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000, // Tamaño máximo de cada chunk en caracteres
chunkOverlap: 150, // Solapamiento entre chunks consecutivos
});
const allSplits = await splitter.splitDocuments(docs);
console.log(`📄 Documentos divididos en ${allSplits.length} chunks\n`);
// =============================================================================
// PASO 4: Eliminar vectores existentes para evitar duplicados
// =============================================================================
// Antes de insertar, eliminamos los vectores que ya existen para las URLs
// que vamos a indexar. Usamos el metadato "source" que contiene la URL.
//
// NOTA: Lo ideal sería verificar si el contenido ha cambiado antes de
// re-indexar (usando un hash o fecha de modificación), pero por simplicidad
// siempre eliminamos y re-insertamos.
console.log("🗑️ Eliminando vectores existentes para evitar duplicados...\n");
const qdrantClient = new QdrantClient({
url: process.env.QDRANT_URL,
apiKey: process.env.QDRANT_API_KEY,
});
// Eliminar vectores por cada URL (filtrando por el metadato "source")
for (const url of urls) {
try {
await qdrantClient.delete(COLLECTION_NAME, {
filter: {
must: [
{
key: "metadata.source",
match: { value: url },
},
],
},
});
console.log(` → Eliminados vectores de: ${url}`);
} catch (error: any) {
// Si la colección no existe, no hay nada que eliminar
if (error.status === 404 || error.message?.includes("not found")) {
console.log(` → Colección no existe aún, se creará al insertar`);
break; // No hace falta seguir intentando eliminar
}
throw error;
}
}
console.log("");
// =============================================================================
// PASO 5: Generar embeddings e insertar en Qdrant
// =============================================================================
// QdrantVectorStore.fromDocuments():
// - Genera embeddings para cada chunk
// - Los inserta en la colección de Qdrant
// - Crea la colección si no existe
console.log("📤 Generando embeddings e insertando en Qdrant...\n");
// Instanciar el vector store (crea la colección si no existe al insertar)
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, {
url: process.env.QDRANT_URL,
collectionName: COLLECTION_NAME,
apiKey: process.env.QDRANT_API_KEY,
});
// Insertamos en lotes para no superar el límite de tokens de Mistral
const BATCH_SIZE = 32;
for (let i = 0; i < allSplits.length; i += BATCH_SIZE) {
const batch = allSplits.slice(i, i + BATCH_SIZE);
console.log(` → Batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(allSplits.length / BATCH_SIZE)} (${batch.length} chunks)`);
await vectorStore.addDocuments(batch);
}
console.log(`${allSplits.length} vectores insertados en "${COLLECTION_NAME}"\n`);
// =============================================================================
// PASO 6: Verificar con una búsqueda de prueba
// =============================================================================
// Hacemos una búsqueda semántica para verificar que todo funciona.
console.log("🔍 Verificando con búsqueda de prueba...\n");
const query = "What does Cooper say about gravity?";
console.log(`Query: "${query}"\n`);
const retrievedDocs = await vectorStore.similaritySearch(query, 2);
console.log("Documentos recuperados:");
retrievedDocs.forEach((doc, idx) => {
console.log(`\n--- Documento ${idx + 1} ---`);
console.log(`Fuente: ${doc.metadata.source}`);
console.log(`Contenido (primeros 300 chars):`);
console.log(doc.pageContent.substring(0, 300) + "...");
});
console.log("\n✅ Indexación completada");
@@ -0,0 +1,251 @@
/**
* =============================================================================
* EJERCICIO: Wrapper de Agente LangChain con Streaming
* =============================================================================
*
* Este archivo implementa un wrapper sobre el agente de LangChain que permite:
* - Gestionar múltiples conversaciones simultáneas (por thread_id/uuid)
* - Streaming de mensajes a clientes conectados vía SSE
* - Memoria persistente de conversaciones (checkpointer)
* - Integración con herramientas (tools) personalizadas
* - Inyección de dependencias vía contextSchema (ej: vectorStore)
*
* ENUNCIADO INICIAL (versión simplificada sin LangChain):
* ─────────────────────────────────────────────────────────────────────────────
* import { AIMessage, BaseMessage, HumanMessage } from "@langchain/core/messages";
*
* export class Agent {
* // Handlers organizados por uuid (thread_id) para soportar múltiples conversaciones
* private messagesHandlers: Record<string, Array<(message: BaseMessage) => void>> = {};
*
* messageReceived(message: string, clientConfig: { thread_id: string }) {
* const { thread_id } = clientConfig;
* const m = new HumanMessage(message);
* this.messagesHandlers[thread_id]?.forEach((handler) => handler(m));
*
* // Simular respuesta del agente después de 6 segundos
* setTimeout(() => {
* const m = new AIMessage(`Respuesta del agente a "${message}"`);
* this.messagesHandlers[thread_id]?.forEach((handler) => handler(m));
* }, 6000);
* }
*
* registerNewMessageHandler(handler: (message: BaseMessage) => void, uuid: string) {
* this.messagesHandlers[uuid] = this.messagesHandlers[uuid] || [];
* this.messagesHandlers[uuid].push(handler);
* }
*
* unregisterNewMessageHandler(uuid: string, handler: (message: BaseMessage) => void) {
* this.messagesHandlers[uuid] = this.messagesHandlers[uuid].filter((h) => h !== handler);
* }
* }
* ─────────────────────────────────────────────────────────────────────────────
*
* CONCEPTOS CLAVE:
* - createAgent(): Crea un agente ReAct con herramientas y middleware
* - Checkpointer: Guarda el estado de la conversación (memoria persistente)
* - stream(): Permite recibir respuestas del agente de forma incremental
* - dynamicSystemPromptMiddleware: Permite generar prompts dinámicos en cada invocación
* - contextSchema: Define el contexto que se pasa a las tools (ej: vectorStore)
* - messagesHandlers: Patrón pub/sub para notificar a clientes conectados
*
* FLUJO:
* 1. Cliente se conecta y registra un handler (registerNewMessageHandler)
* 2. Cliente envía mensaje (messageReceived)
* 3. Agente procesa el mensaje y genera respuesta (puede usar tools)
* 4. Cada chunk de respuesta se envía a los handlers registrados
* 5. Cliente se desconecta (unregisterNewMessageHandler)
* =============================================================================
*/
import { Calculator } from "@langchain/community/tools/calculator";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { VectorStore } from "@langchain/core/vectorstores";
import { BaseCheckpointSaver, MemorySaver } from "@langchain/langgraph";
import { createAgent, dynamicSystemPromptMiddleware } from "langchain";
import { ConfigurableModel } from "langchain/chat_models/universal";
import {
AgentContext,
agentContextSchema,
buildFilmDocumentTool,
getExchangeRatesTool,
getHistoricalRatesTool,
storageKnowledgeTool,
} from "../helpers/tools";
// =============================================================================
// CLASE AGENT: Wrapper del agente LangChain
// =============================================================================
export class Agent {
// Handlers organizados por uuid (thread_id) para soportar múltiples conversaciones
// Cada conversación tiene su propio array de handlers (clientes conectados)
private messagesHandlers: Record<
string,
Array<(message: BaseMessage | any) => void>
> = {};
// Agente interno de LangChain (ReAct Agent)
// Usamos ReturnType para inferir el tipo correcto de createAgent
private internalAgent: ReturnType<typeof createAgent>;
// Vector store para las búsquedas RAG (se pasa como contexto a las tools)
private vectorStore: VectorStore;
/**
* Constructor del agente
* @param model Modelo de lenguaje LangChain (Mistral, OpenAI, etc.)
* @param vectorStore Vector store para búsquedas RAG (se inyecta en las tools)
* @param checkpointer Persistencia de conversaciones (SQLite, Memory, etc.)
*/
constructor(
model: ConfigurableModel,
vectorStore: VectorStore,
checkpointer: BaseCheckpointSaver = new MemorySaver(),
) {
this.vectorStore = vectorStore;
// createAgent() crea un agente ReAct que puede usar herramientas
this.internalAgent = createAgent({
model,
// contextSchema define la estructura del contexto que se pasa a las tools
// Las tools acceden a él vía config.context (ej: config.context.vectorStore)
contextSchema: agentContextSchema,
// Herramientas disponibles para el agente
tools: [
storageKnowledgeTool, // RAG sobre web storage (usa vectorStore del contexto)
getExchangeRatesTool, // Tasas de cambio actuales
getHistoricalRatesTool, // Tasas de cambio históricas
buildFilmDocumentTool, // Herramienta personalizada para generar documentos de películas
new Calculator(), // Calculadora matemática
],
// Middleware: funciones que procesan los mensajes antes/después del LLM
middleware: [
//trimMessages, // Limita el historial para no exceder el contexto
// System prompt dinámico: se ejecuta en cada invocación
// Esto permite incluir información que cambia (fecha, estado, etc.)
dynamicSystemPromptMiddleware(() => {
const today = new Date().toLocaleDateString("es-ES", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
return `Eres un asistente experto y preciso. Responde siempre en español de forma clara y concisa.
FECHA ACTUAL: ${today}
HERRAMIENTAS DISPONIBLES Y CUÁNDO USARLAS:
1. **storage_knowledge** - Base de conocimiento sobre almacenamiento web
USAR OBLIGATORIAMENTE cuando el usuario pregunte sobre peliculas de Christopher Nolan:
- Interstellar
- Inception
2. **get_exchange_rates** - Tasas de cambio ACTUALES
USAR cuando el usuario pregunte sobre:
- Valor actual de una divisa (ej: "¿Cuánto vale el dólar hoy?")
- Conversión de monedas al día de hoy
3. **get_historical_rates** - Tasas de cambio HISTÓRICAS
USAR cuando el usuario pregunte sobre:
- Valor de una divisa en una fecha pasada específica
- Comparación de evolución de divisas entre fechas
4. **calculator** - Calculadora matemática
USAR para cualquier cálculo numérico que requiera precisión
5. **build_film_document** - Generador de documentos de películas
USAR cuando el usuario quiera generar un documento detallado sobre una película específica
REGLAS:
- Si no estás seguro de la respuesta, USA las herramientas disponibles
- Para preguntas sobre peliculas de nolan, SIEMPRE consulta storage_knowledge primero
- Para conversiones de moneda, USA las herramientas de tasas de cambio
- Sé conciso pero completo en tus respuestas`;
}),
],
// Persistencia de la conversación
checkpointer,
});
}
// ===========================================================================
// messageReceived: Procesa un mensaje del usuario
// ===========================================================================
// Recibe un mensaje, lo pasa al agente y envía las respuestas vía streaming.
async messageReceived(message: string, clientConfig: { thread_id: string }) {
const { thread_id } = clientConfig;
const initialMessage = new HumanMessage(message);
this._sendMessageToClients(thread_id, initialMessage);
console.log(`[Agent] Mensaje recibido en thread: ${thread_id}`);
// stream() permite recibir respuestas incrementales del agente
// streamMode: "values" devuelve el estado completo en cada chunk
// context: pasa el vectorStore a las tools que lo necesiten
const response = await this.internalAgent.stream(
{ messages: [initialMessage] },
{
streamMode: "updates",
configurable: { thread_id },
// El contexto se pasa a las tools vía config.context
context: { vectorStore: this.vectorStore } satisfies AgentContext,
},
);
// Procesar cada chunk del streaming
// Con streamMode "updates", el chunk tiene forma { nodeName: { messages: [...] } }
for await (const chunk of response) {
const nodeOutput = Object.values(chunk)[0] as {
messages?: BaseMessage[];
};
const messages = nodeOutput?.messages ?? [];
for (const msg of messages) {
this._sendMessageToClients(thread_id, msg);
}
}
}
// ===========================================================================
// _sendMessageToClients: Notifica a todos los handlers de una conversación
// ===========================================================================
private _sendMessageToClients(uuid: string, message: BaseMessage | any) {
// Verificar que existan handlers para este uuid
if (!this.messagesHandlers[uuid]) return;
this.messagesHandlers[uuid].forEach((handler) => handler(message));
}
// ===========================================================================
// registerNewMessageHandler: Registra un handler para recibir mensajes
// ===========================================================================
// Se llama cuando un cliente abre una conexión SSE.
registerNewMessageHandler(
handler: (message: BaseMessage | any) => void,
uuid: string,
) {
this.messagesHandlers[uuid] = this.messagesHandlers[uuid] || [];
this.messagesHandlers[uuid].push(handler);
console.log(`[Agent] Handler registrado para thread: ${uuid}`);
}
// ===========================================================================
// unregisterNewMessageHandler: Elimina un handler cuando el cliente se desconecta
// ===========================================================================
unregisterNewMessageHandler(
uuid: string,
handler: (message: BaseMessage | any) => void,
) {
if (!this.messagesHandlers[uuid]) return;
this.messagesHandlers[uuid] = this.messagesHandlers[uuid].filter(
(h) => h !== handler,
);
console.log(`[Agent] Handler desregistrado para thread: ${uuid}`);
}
}
@@ -0,0 +1,8 @@
export const comments = [
{ uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", content: "¡Excelente artículo! Me ha ayudado muchísimo a entender el tema. Gracias por compartir." },
{ uuid: "b2c3d4e5-f6a7-8901-bcde-f12345678901", content: "Meh, está bien pero tampoco es nada del otro mundo. He leído cosas mejores." },
{ uuid: "c3d4e5f6-a7b8-9012-cdef-123456789012", content: "No estoy de acuerdo con el punto 3, creo que habría que matizarlo un poco más." },
{ uuid: "d4e5f6a7-b8c9-0123-def0-234567890123", content: "Vaya mierda de artículo, no tienes ni puta idea de lo que hablas." },
{ uuid: "e5f6a7b8-c9d0-1234-ef01-345678901234", content: "Interesante perspectiva. ¿Podrías ampliar la información sobre el segundo apartado?" },
{ uuid: "f6a7b8c9-d0e1-2345-f012-456789012345", content: "Llevo años trabajando en esto y confirmo que todo lo que dices es correcto. Gran trabajo." }
]
@@ -0,0 +1,3 @@
import { resolve } from "node:path";
export const OUTPUT_DIR = resolve("output_docx");
+18
View File
@@ -0,0 +1,18 @@
import { createInterface } from "node:readline/promises";
export async function promptUser(
query: string,
defaultValue = ""
): Promise<string> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const response = await rl.question(
`${query} ${defaultValue ? `[${defaultValue}] ` : ""}`
);
rl.close();
return response || defaultValue;
}
@@ -0,0 +1,44 @@
import {
RemoveMessage,
ToolMessage
} from "@langchain/core/messages";
import { REMOVE_ALL_MESSAGES } from "@langchain/langgraph";
import { createMiddleware } from "langchain";
export const trimMessages = createMiddleware({
name: "TrimMessages",
beforeModel: (state) => {
const messages = state.messages;
if (messages.length <= 3) return; // No recortar si hay pocos
// Mantener primer mensaje + últimos 4
const firstMsg = messages[0];
let recentMsgs = messages.slice(-4);
// Asegurar que no empezamos con un mensaje 'tool' huérfano.
// Si el primer mensaje reciente es de tipo 'tool', necesitamos incluir
// el mensaje 'assistant' con tool_calls que lo precede.
while (recentMsgs.length > 0 && ToolMessage.isInstance(recentMsgs[0])) {
// Buscar el índice en el array original
const idx = messages.indexOf(recentMsgs[0]);
if (idx > 0) {
// Incluir el mensaje anterior (debería ser assistant con tool_calls)
recentMsgs = [messages[idx - 1], ...recentMsgs];
} else {
// No hay mensaje anterior, eliminar el tool huérfano
recentMsgs = recentMsgs.slice(1);
}
}
// También asegurar que no empezamos con un 'assistant' sin contexto previo
// después del primer mensaje (esto es menos problemático pero más limpio)
const newMessages = [firstMsg, ...recentMsgs];
return {
messages: [
new RemoveMessage({ id: REMOVE_ALL_MESSAGES }),
...newMessages,
],
};
},
});
+287
View File
@@ -0,0 +1,287 @@
/**
* =============================================================================
* HERRAMIENTAS (TOOLS) PARA AGENTES DE LANGCHAIN
* =============================================================================
*
* Este archivo contiene las herramientas que los agentes pueden usar para
* realizar tareas específicas. Las Tools permiten al LLM interactuar con
* el mundo exterior: APIs, bases de datos, cálculos, etc.
*
* CONCEPTOS CLAVE:
* - tool(): Función para crear herramientas personalizadas
* - description: El LLM usa esta descripción para decidir CUÁNDO usar la tool
* - schema: Define los parámetros que acepta la herramienta (validados con Zod)
* - config: Segundo parámetro de la tool que permite acceder al contexto runtime
*
* BUENAS PRÁCTICAS PARA DESCRIPTIONS:
* - Ser específico sobre cuándo usar la herramienta
* - Indicar qué tipo de información devuelve
* - Usar mayúsculas para enfatizar casos de uso obligatorios
*
* RUNTIME CONTEXT:
* Las tools pueden acceder al contexto del agente a través del parámetro `config`.
* Esto permite inyectar dependencias (como vectorStore, DB connections, etc.)
* en tiempo de ejecución, evitando estado global y haciendo las tools más testables.
* =============================================================================
*/
import { CohereRerank } from "@langchain/cohere";
import { WikipediaQueryRun } from "@langchain/community/tools/wikipedia_query_run";
import { DocumentInterface } from "@langchain/core/documents";
import { VectorStore } from "@langchain/core/vectorstores";
import { ChatMistralAI } from "@langchain/mistralai";
import { HumanMessage, SystemMessage, tool } from "langchain";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { z } from "zod";
import Docxtemplater from "docxtemplater";
import { join } from "node:path";
import PizZip from "pizzip";
import { OUTPUT_DIR } from "./constants";
// =============================================================================
// CONTEXT SCHEMA: Define la estructura del contexto del agente
// =============================================================================
// Este esquema se usa en createAgent() para tipar el contexto que se pasa
// al invocar el agente. Las tools acceden a él vía config.context
//
// Ejemplo de uso:
// const agent = createAgent({ ..., contextSchema: agentContextSchema });
// agent.invoke({ messages }, { context: { vectorStore: myVectorStore } });
export const agentContextSchema = z.object({
vectorStore: z
.custom<VectorStore>()
.describe("Vector store para búsquedas RAG"),
});
// Tipo TypeScript inferido del esquema
export type AgentContext = z.infer<typeof agentContextSchema>;
// =============================================================================
// TOOL: Contador de Palabras
// =============================================================================
// Herramienta síncrona simple para contar palabras en un texto.
// Útil porque los LLMs no pueden contar con precisión.
export const wordCountTool = tool(
({ texto }) => {
process.stdout.write("Contando palabras...");
const words = texto.trim().split(/\s+/).filter(Boolean).length;
process.stdout.write(` [${words}]\n`);
return words.toString();
},
{
name: "contar_palabras",
description:
"Cuenta el número exacto de palabras en un texto. USAR SIEMPRE que necesites saber la cantidad de palabras de un texto, ya que no puedes contarlas con precisión por ti mismo.",
schema: z.object({
texto: z.string().describe("El texto del cual contar las palabras"),
}),
},
);
// =============================================================================
// TOOL: Wikipedia
// =============================================================================
// Herramienta pre-construida de LangChain para buscar en Wikipedia.
// Útil para obtener información general y actualizada sobre cualquier tema.
export const wikipediaTool = new WikipediaQueryRun({
topKResults: 3, // Número máximo de resultados a devolver
maxDocContentLength: 4000, // Longitud máxima del contenido por documento
});
// =============================================================================
// TOOL: Tasas de Cambio Actuales
// =============================================================================
// Consulta la API de Frankfurter para obtener tasas de cambio en tiempo real.
// API gratuita y sin autenticación: https://www.frankfurter.app/
export const getExchangeRatesTool = tool(
async ({ base, symbols }) => {
const url = `https://api.frankfurter.app/latest?from=${base}&to=${symbols.join(",")}`;
const response = await fetch(url);
const data = await response.json();
return JSON.stringify(data);
},
{
name: "get_exchange_rates",
description:
"Obtiene las tasas de cambio ACTUALES desde una moneda base a otras monedas. Usar cuando el usuario pregunte por el valor actual de una divisa o quiera convertir cantidades entre monedas HOY.",
schema: z.object({
base: z
.string()
.describe("Código ISO de la moneda base (ej: EUR, USD, GBP, JPY)"),
symbols: z
.array(z.string())
.describe(
"Array de códigos ISO de las monedas destino (ej: ['USD', 'GBP'])",
),
}),
},
);
// =============================================================================
// TOOL: Tasas de Cambio Históricas
// =============================================================================
// Consulta tasas de cambio de una fecha específica en el pasado.
// Útil para comparar evolución de divisas o consultas sobre fechas concretas.
export const getHistoricalRatesTool = tool(
async ({ date, base, symbols }) => {
const url = `https://api.frankfurter.dev/v1/${date}?from=${base}&to=${symbols.join(",")}`;
console.log(`Consultando tasas de cambio históricas con URL: ${url}`);
const response = await fetch(url);
const data = await response.json();
console.log(data);
return JSON.stringify(data);
},
{
name: "get_historical_rates",
description:
"Obtiene tasas de cambio de una fecha PASADA específica. Usar cuando el usuario pregunte por el valor de una divisa en una fecha concreta, o quiera comparar la evolución de una moneda entre dos fechas.",
schema: z.object({
date: z.string().describe("Fecha en formato YYYY-MM-DD (ej: 2024-01-15)"),
base: z.string().describe("Código ISO de la moneda base (ej: EUR, USD)"),
symbols: z
.array(z.string())
.describe("Array de códigos ISO de las monedas destino"),
}),
},
);
// =============================================================================
// TOOL: Base de Conocimiento sobre Web Storage
// =============================================================================
// Herramienta RAG que consulta una base de datos vectorial con documentación
// sobre sistemas de almacenamiento en navegadores (localStorage, IndexedDB, etc.)
//
// IMPORTANTE: Esta tool accede al vectorStore desde el contexto del agente
// en lugar de usar una variable global. Esto hace la tool más testable y
// permite usar diferentes vector stores según el contexto.
//
// El vectorStore se pasa al invocar el agente:
// agent.invoke({ messages }, { context: { vectorStore: myVectorStore } });
const mistralMini = new ChatMistralAI({
model: "mistral-tiny",
}).withStructuredOutput(
z.object({
querys: z
.array(z.string())
.describe("Consulta del usuario para buscar en la base de conocimiento"),
}),
);
export const storageKnowledgeTool = tool(
async ({ query }, config) => {
process.stdout.write(`Buscando en base de conocimiento: "${query}"\n`);
const querys = await mistralMini.invoke([
new SystemMessage(
`A partir del mensaje del usuario, necesito una colección de palabras (una o dos) relevantes semanticamente relacionadas, para encontrar lo que necesita el usuario, buscando en una bbdd vectorial con guiones de películas.`,
),
new HumanMessage(query),
]);
process.stdout.write(
` → Busquedas generadas por Mistral: ${JSON.stringify(querys)}\n`,
);
// Acceder al vectorStore desde el contexto del agente
// config.context contiene los valores pasados en { context: {...} } al invocar
const vectorStore = (config as any).context?.vectorStore as VectorStore;
if (!vectorStore) {
return "Error: No se ha configurado el vector store en el contexto del agente.";
}
const docs: Array<DocumentInterface> = [];
for (const q of querys.querys) {
process.stdout.write(` → Buscando en vector store con query: "${q}"\n`);
const retrievedDocs = await vectorStore.similaritySearch(q, 3);
process.stdout.write(
`${retrievedDocs.length} documentos encontrados\n`,
);
if (retrievedDocs.length > 0) {
docs.push(...retrievedDocs);
}
}
if (docs.length === 0) {
return `No se encontró información relevante sobre "${query}".`;
}
const reranker = new CohereRerank({
apiKey: process.env.COHERE_API_KEY,
model: "rerank-v3.5",
topN: 5,
});
const rerankedDocs = await reranker.compressDocuments(docs, query);
console.log(` → Documentos reordenados por relevancia con Cohere Rerank`);
// Formatear los documentos recuperados como contexto
const context = rerankedDocs
.map((doc) => doc.pageContent)
.join("\n\n---\n\n");
return `Información encontrada sobre "${query}":\n\n${context}`;
},
{
name: "storage_knowledge",
description:
"Consulta la base de conocimiento sobre peliculas de Christopher Nolan. USAR OBLIGATORIAMENTE cuando el usuario pregunte sobre: Interstellar, Inception. Devuelve información relevante extraída de documentos relacionados con esas películas.",
schema: z.object({
query: z
.string()
.describe(
"Pregunta o tema a buscar sobre peliculas de Christopher Nolan",
),
}),
},
);
/**
*
*/
export const buildFilmDocumentTool = tool(
async (data) => {
const content = readFileSync("template/plantilla_ficha.docx", "binary");
const zip = new PizZip(content);
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
});
doc.render({
...data,
fechaGeneracion: new Date().toLocaleDateString("es-ES"),
});
mkdirSync(OUTPUT_DIR, { recursive: true });
const buf = doc.getZip().generate({ type: "nodebuffer" });
writeFileSync(join(OUTPUT_DIR, `${data.titulo}.docx`), buf);
return `Documento generado. href de descarga => "http://localhost:3000/files/${data.titulo}.docx"`;
},
{
name: "build_film_document",
description: `Construye un documento a partir de los datos de una película.
USAR CUANDO necesites crear un documento con información estructurada sobre una película, incluyendo título, director, año, género y sinopsis.
El resultado debe ser un JSON con formato específico.`,
schema: z.object({
titulo: z.string().describe("Título de la película"),
genero: z.string().describe("Género de la película"),
duracion: z.string().describe("Duración en minutos"),
director: z.string().describe("Director o directora principal"),
year: z.string().describe("Año de la propducción"),
sinopsis: z.string().describe("Sinopsis de 3-4 frases"),
reparto: z
.array(z.object({ item: z.string() }))
.describe("Lista de miembros del reparto"),
}),
},
);
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "esnext",
"target": "es2022",
"moduleResolution": "bundler",
"types": ["node"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"moduleDetection": "force"
},
"include": ["src", "../samples"]
}
+8
View File
@@ -0,0 +1,8 @@
node_modules/
dist/
.env
.env.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
+1
View File
@@ -0,0 +1 @@
v24.11.1
+26
View File
@@ -0,0 +1,26 @@
# Slides LangChain.js
Presentación sobre LangChain.js - Desarrollo de aplicaciones con LLMs.
## Ver online
https://nykk.io/slides-langchain-eitb/
## Desarrollo local
```bash
npm install
npm run dev
```
## Build
```bash
npm run build
```
## Deploy
```bash
npm run deploy
```
+14
View File
@@ -0,0 +1,14 @@
<script setup>
import { useNav } from '@slidev/client'
const { currentPage } = useNav()
</script>
<template>
<div
v-if="currentPage > 1"
style="position:fixed;top:10px;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:0 28px;z-index:100;pointer-events:none;opacity:0.22;"
>
<img src="/EITB.svg" style="height:14px;width:auto;object-fit:contain;" />
<img src="/nykk.png" style="height:8px;width:auto;object-fit:contain;" />
</div>
</template>
+9382
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "slides-langchain.js",
"type": "module",
"private": true,
"scripts": {
"build": "slidev build",
"dev": "slidev --open --remote",
"export": "slidev export",
"preview": "slidev slides.md",
"deploy": "git push github main"
},
"dependencies": {
"@slidev/cli": "^52.14.1",
"@slidev/theme-default": "latest",
"@slidev/theme-seriph": "latest",
"vue": "^3.5.30"
},
"devDependencies": {
"playwright-chromium": "^1.58.2"
}
}
+14
View File
@@ -0,0 +1,14 @@
---
layout: cover
class: text-center
---
# Módulo 5: Agentes
Tools, Structured Output, Runtime y Human-in-the-Loop
<div class="pt-12">
<span class="text-xl opacity-80">
LLMs que pueden actuar en el mundo real
</span>
</div>
+41
View File
@@ -0,0 +1,41 @@
---
layout: default
---
# ¿Qué es un Agente?
<div class="mt-10 text-sm">
<v-click>
<div class="mb-8">
Un **agente** es un sistema que usa un LLM para decidir qué acciones tomar
</div>
</v-click>
<div class="grid grid-cols-2 gap-8">
<v-click>
<div class="border-l-4 border-red-500 pl-4 bg-red-50 dark:bg-red-900/20 p-4">
<h3 class="text-base font-bold mb-3">❌ Sin Agentes</h3>
<p class="text-xs mb-2">Flujo fijo y predecible</p>
<p class="text-xs mb-2">Usuario → LLM → Respuesta</p>
<p class="text-xs opacity-70">El LLM solo genera texto</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-4">
<h3 class="text-base font-bold mb-3">✅ Con Agentes</h3>
<p class="text-xs mb-2">Flujo dinámico y adaptativo</p>
<p class="text-xs mb-2">Usuario → LLM → Tool → LLM → Respuesta</p>
<p class="text-xs opacity-70">El LLM decide qué herramientas usar</p>
</div>
</v-click>
</div>
</div>
+37
View File
@@ -0,0 +1,37 @@
---
layout: default
---
# Casos de Uso de Agentes
<div class="mt-6 grid grid-cols-2 gap-4 text-xs">
<v-click>
<div class="border-l-4 border-blue-500 pl-3 bg-blue-50 dark:bg-blue-900/20 p-2">
<h3 class="text-sm font-bold mb-1">🔍 Búsqueda y análisis</h3>
<p>Buscar información en APIs, bases de datos o web</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-3 bg-green-50 dark:bg-green-900/20 p-2">
<h3 class="text-sm font-bold mb-1">🛠️ Automatización</h3>
<p>Enviar emails, crear tickets, actualizar sistemas</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-3 bg-purple-50 dark:bg-purple-900/20 p-2">
<h3 class="text-sm font-bold mb-1">💼 Asistentes empresariales</h3>
<p>Consultar CRM, generar reportes, analizar datos</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-orange-500 pl-3 bg-orange-50 dark:bg-orange-900/20 p-2">
<h3 class="text-sm font-bold mb-1">🤖 Agentes autónomos</h3>
<p>Tomar decisiones complejas con múltiples pasos</p>
</div>
</v-click>
</div>
+45
View File
@@ -0,0 +1,45 @@
---
layout: default
---
# createAgent: Creando el Agente
<div class="mt-6 text-xs">
<v-click>
<div class="mb-4">
**createAgent** es la forma estándar de crear agentes en LangChain v1
</div>
</v-click>
<v-click>
```typescript
import { createAgent } from "langchain";
const agent = createAgent({
model: "mistral:mistral-large-latest", // Modelo como string
tools: [weatherTool], // Array de tools
systemPrompt: "Eres un asistente útil.",
});
const result = await agent.invoke({
messages: [{ role: "user", content: "¿Qué tiempo hace en Madrid?" }]
});
```
</v-click>
<v-click>
<div class="mt-4 text-xs opacity-70">
El agente automatiza el ciclo: llamar al LLM -> ejecutar tools -> procesar resultados -> repetir
</div>
</v-click>
</div>
+42
View File
@@ -0,0 +1,42 @@
---
layout: default
---
# Configuración del Modelo
<div class="mt-4 text-xs">
<v-click>
**Opción 1: String con formato `provider:model`**
```typescript
const agent = createAgent({
model: "mistral:mistral-large-latest", // Mistral
// model: "openai:gpt-4.1", // OpenAI
// model: "anthropic:claude-sonnet-4-6", // Anthropic
tools: [],
});
```
</v-click>
<v-click>
**Opción 2: Instancia del modelo con configuración avanzada**
```typescript
import { ChatMistralAI } from "@langchain/mistralai";
const model = new ChatMistralAI({
model: "mistral-large-latest",
temperature: 0.1,
maxTokens: 1000,
});
const agent = createAgent({ model, tools: [] });
```
</v-click>
</div>
+43
View File
@@ -0,0 +1,43 @@
---
layout: default
---
# systemPrompt: Configurando el Comportamiento
<div class="mt-4 text-xs">
<v-click>
**String simple:**
```typescript
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [weatherTool],
systemPrompt: "Eres un asistente meteorológico. Sé conciso y preciso.",
});
```
</v-click>
<v-click>
**SystemMessage (para features avanzados como cache de Anthropic):**
```typescript
import { SystemMessage } from "langchain";
const agent = createAgent({
model: "anthropic:claude-sonnet-4-6",
tools: [weatherTool],
systemPrompt: new SystemMessage({
content: [
{ type: "text", text: "Eres un experto en clima...", cache_control: { type: "ephemeral" } }
],
}),
});
```
</v-click>
</div>
+44
View File
@@ -0,0 +1,44 @@
---
layout: default
---
# Tools: Definiendo Herramientas
<div class="mt-6 text-xs">
<v-click>
```typescript
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const weatherTool = tool(
async ({ location }) => {
// Llamar a API del tiempo
return `El tiempo en ${location} es soleado, 22°C`;
},
{
name: "get_weather",
description: "Obtiene el tiempo actual de una ubicación",
schema: z.object({
location: z.string().describe("Ciudad o ubicación"),
}),
}
);
```
</v-click>
<v-click>
```typescript
// El agente decide automáticamente cuándo usar cada tool
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [weatherTool, calculatorTool, searchTool],
});
```
</v-click>
</div>
+42
View File
@@ -0,0 +1,42 @@
---
layout: default
---
# Streaming: Modos de Transmisión
<div class="mt-4 text-xs">
<v-click>
**`updates`**: Emite evento después de cada paso del agente
```typescript
for await (const chunk of await agent.stream(
{ messages: [{ role: "user", content: "¿Tiempo en Madrid?" }] },
{ streamMode: "updates" }
)) {
console.log(chunk);
// { model: { messages: [AIMessage con tool_call] } }
// { tools: { messages: [ToolMessage con resultado] } }
// { model: { messages: [AIMessage respuesta final] } }
}
```
</v-click>
<v-click>
**`values`**: Emite el estado completo después de cada paso
```typescript
for await (const chunk of await agent.stream(
{ messages: [{ role: "user", content: "¿Tiempo en Madrid?" }] },
{ streamMode: "values" }
)) {
console.log(chunk.messages); // Array completo de mensajes hasta ese punto
}
```
</v-click>
</div>
+40
View File
@@ -0,0 +1,40 @@
---
layout: default
---
# Streaming: Tokens y Mensajes
<div class="mt-4 text-xs">
<v-click>
**`messages`**: Stream de tokens del LLM + metadata
```typescript
for await (const [mode, chunk] of await agent.stream(
{ messages: [{ role: "user", content: "Explica qué es TypeScript" }] },
{ streamMode: ["updates", "messages"] }
)) {
if (mode === "messages") {
const [token, metadata] = chunk;
if (token.content) {
process.stdout.write(token.content); // Token a token
}
} else if (mode === "updates") {
// Paso completo del agente
console.log("\n[Paso completado]", Object.keys(chunk));
}
}
```
</v-click>
<v-click>
<div class="mt-4 text-xs opacity-80">
Combina múltiples modos: `streamMode: ["updates", "messages"]`
</div>
</v-click>
</div>
+39
View File
@@ -0,0 +1,39 @@
---
layout: default
---
# Checkpointer: MemorySaver
<div class="mt-4 text-xs">
<v-click>
**MemorySaver**: Persistencia en memoria (desarrollo/testing)
```typescript
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [weatherTool],
checkpointer: new MemorySaver(), // En memoria RAM
});
const config = { configurable: { thread_id: "conv-1" } };
// Primera conversación
await agent.invoke(
{ messages: [{ role: "user", content: "Hola, soy María" }] }, config
);
// Segunda conversación - recuerda el contexto
await agent.invoke(
{ messages: [{ role: "user", content: "¿Cómo me llamo?" }] }, config
);
// "Tu nombre es María"
```
</v-click>
</div>
+47
View File
@@ -0,0 +1,47 @@
---
layout: default
---
# Checkpointer: SqliteSaver
<div class="mt-4 text-xs">
<v-click>
**SqliteSaver**: Persistencia en SQLite (local)
```bash
npm install @langchain/langgraph-checkpoint-sqlite
```
</v-click>
<v-click>
```typescript
import { createAgent } from "langchain";
import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";
const checkpointer = SqliteSaver.fromConnString("./agent_memory.db");
await checkpointer.setup(); // Crear tablas
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [weatherTool],
checkpointer,
});
// El estado persiste aunque reinicies la aplicación
```
</v-click>
<v-click>
<div class="mt-4 text-xs opacity-80">
Ideal para desarrollo local y aplicaciones de usuario único
</div>
</v-click>
</div>
+47
View File
@@ -0,0 +1,47 @@
---
layout: default
---
# Checkpointer: PostgresSaver
<div class="mt-4 text-xs">
<v-click>
**PostgresSaver**: Persistencia en PostgreSQL (producción)
```bash
npm install @langchain/langgraph-checkpoint-postgres
```
</v-click>
<v-click>
```typescript
import { createAgent } from "langchain";
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
const DB_URI = "postgresql://user:pass@localhost:5432/mydb";
const checkpointer = PostgresSaver.fromConnString(DB_URI);
await checkpointer.setup();
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [weatherTool],
checkpointer,
});
```
</v-click>
<v-click>
<div class="mt-4 text-xs opacity-80">
También disponibles: **MongoDB** (`@langchain/langgraph-checkpoint-mongodb`),
**Redis** (`@langchain/langgraph-checkpoint-redis`), y **Custom** (implementar `BaseCheckpointSaver`)
</div>
</v-click>
</div>
+51
View File
@@ -0,0 +1,51 @@
---
layout: default
---
# Middleware: Arquitectura
<div class="mt-6 text-xs">
<v-click>
<div class="mb-4">
**Middleware**: Hooks que interceptan el flujo del agente en puntos clave
</div>
</v-click>
<v-click>
<div class="space-y-3 text-xs">
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-2">
<code class="font-bold">beforeAgent</code> - Antes de iniciar el agente
</div>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-2">
<code class="font-bold">beforeModel</code> - Antes de cada llamada al LLM
</div>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-2">
<code class="font-bold">wrapModelCall</code> - Envuelve la llamada al LLM (interceptar/modificar)
</div>
<div class="border-l-4 border-orange-500 pl-4 bg-orange-50 dark:bg-orange-900/20 p-2">
<code class="font-bold">wrapToolCall</code> - Envuelve la ejecución de tools
</div>
<div class="border-l-4 border-red-500 pl-4 bg-red-50 dark:bg-red-900/20 p-2">
<code class="font-bold">afterModel</code> - Después de cada respuesta del LLM
</div>
<div class="border-l-4 border-cyan-500 pl-4 bg-cyan-50 dark:bg-cyan-900/20 p-2">
<code class="font-bold">afterAgent</code> - Después de completar el agente
</div>
</div>
</v-click>
</div>
+36
View File
@@ -0,0 +1,36 @@
---
layout: default
---
# Middleware Custom
<div class="mt-4 text-xs">
<v-click>
```typescript
import { createAgent, createMiddleware } from "langchain";
const loggingMiddleware = createMiddleware({
name: "LoggingMiddleware",
beforeModel: (state) => {
console.log(`Llamando modelo con ${state.messages.length} mensajes`);
return; // No modificar estado
},
afterModel: (state) => {
const lastMsg = state.messages[state.messages.length - 1];
console.log(`Modelo respondió: ${lastMsg.content?.slice(0, 50)}...`);
return;
},
});
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [weatherTool],
middleware: [loggingMiddleware],
});
```
</v-click>
</div>
+46
View File
@@ -0,0 +1,46 @@
---
layout: default
---
# dynamicSystemPromptMiddleware
<div class="mt-4 text-xs">
<v-click>
```typescript
import * as z from "zod";
import { createAgent, dynamicSystemPromptMiddleware } from "langchain";
const contextSchema = z.object({
userRole: z.enum(["expert", "beginner"]),
});
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [searchTool],
contextSchema,
middleware: [
dynamicSystemPromptMiddleware<z.infer<typeof contextSchema>>((state, runtime) => {
const userRole = runtime.context.userRole;
let prompt = "Eres un asistente útil.";
if (userRole === "expert") {
prompt += " Proporciona respuestas técnicas detalladas.";
} else {
prompt += " Explica conceptos de forma simple, sin jerga técnica.";
}
return prompt;
}),
],
});
await agent.invoke(
{ messages: [{ role: "user", content: "Explica machine learning" }] },
{ context: { userRole: "beginner" } }
);
```
</v-click>
</div>
+48
View File
@@ -0,0 +1,48 @@
---
layout: default
---
# toolCallLimitMiddleware: Limitar Tools
<div class="mt-4 text-xs">
<v-click>
```typescript
import { createAgent, toolCallLimitMiddleware } from "langchain";
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [searchTool, databaseTool, scraperTool],
middleware: [
// Límite global: máx 20 por thread, 10 por run
toolCallLimitMiddleware({ threadLimit: 20, runLimit: 10 }),
// Límite específico para búsquedas
toolCallLimitMiddleware({
toolName: "search",
threadLimit: 5,
runLimit: 3,
}),
// Límite estricto para scraping (lanza error si excede)
toolCallLimitMiddleware({
toolName: "scrape_webpage",
runLimit: 2,
exitBehavior: "error", // "continue" | "error" | "end"
}),
],
});
```
</v-click>
<v-click>
<div class="mt-3 text-xs opacity-80">
Requiere checkpointer para `threadLimit`. `runLimit` se resetea en cada invocación.
</div>
</v-click>
</div>
+44
View File
@@ -0,0 +1,44 @@
---
layout: default
---
# summarizationMiddleware: Resumir Historial
<div class="mt-4 text-xs">
<v-click>
```typescript
import { createAgent, summarizationMiddleware } from "langchain";
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [searchTool, calculatorTool],
middleware: [
summarizationMiddleware({
model: "mistral:mistral-small-latest", // Modelo para resumir (más barato)
trigger: { tokens: 4000 }, // Resumir cuando supere 4000 tokens
keep: { messages: 20 }, // Mantener los últimos 20 mensajes
}),
],
});
```
</v-click>
<v-click>
<div class="mt-4 text-xs opacity-80">
**Funcionamiento automático:**
1. Detecta cuando el historial supera el límite de tokens
2. Resume los mensajes antiguos usando el modelo especificado
3. Reemplaza permanentemente los mensajes antiguos por el resumen
4. Mantiene los mensajes recientes intactos para contexto
</div>
</v-click>
</div>
+46
View File
@@ -0,0 +1,46 @@
---
layout: default
---
# Middleware: Limitar Historial (Trim)
<div class="mt-4 text-xs">
<v-click>
```typescript
import { RemoveMessage } from "@langchain/core/messages";
import { createAgent, createMiddleware } from "langchain";
import { MemorySaver, REMOVE_ALL_MESSAGES } from "@langchain/langgraph";
const trimMessages = createMiddleware({
name: "TrimMessages",
beforeModel: (state) => {
const messages = state.messages;
if (messages.length <= 5) return; // No recortar si hay pocos
// Mantener primer mensaje + últimos 4
const firstMsg = messages[0];
const recentMsgs = messages.slice(-4);
const newMessages = [firstMsg, ...recentMsgs];
return {
messages: [
new RemoveMessage({ id: REMOVE_ALL_MESSAGES }),
...newMessages,
],
};
},
});
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [],
middleware: [trimMessages],
checkpointer: new MemorySaver(),
});
```
</v-click>
</div>
+40
View File
@@ -0,0 +1,40 @@
---
layout: default
---
# Middleware: wrapModelCall
<div class="mt-4 text-xs">
<v-click>
Permite interceptar y modificar la llamada al modelo:
```typescript
import { ChatMistralAI } from "@langchain/mistralai";
import { createAgent, createMiddleware } from "langchain";
const basicModel = new ChatMistralAI({ model: "mistral-small-latest" });
const advancedModel = new ChatMistralAI({ model: "mistral-large-latest" });
const dynamicModelSelection = createMiddleware({
name: "DynamicModelSelection",
wrapModelCall: (request, handler) => {
// Elegir modelo según complejidad de la conversación
const messageCount = request.messages.length;
const model = messageCount > 10 ? advancedModel : basicModel;
return handler({ ...request, model });
},
});
const agent = createAgent({
model: basicModel,
tools: [searchTool],
middleware: [dynamicModelSelection],
});
```
</v-click>
</div>
+40
View File
@@ -0,0 +1,40 @@
---
layout: default
---
# Middleware: wrapToolCall
<div class="mt-4 text-xs">
<v-click>
Intercepta la ejecución de tools para manejo de errores:
```typescript
import { createAgent, createMiddleware, ToolMessage } from "langchain";
const handleToolErrors = createMiddleware({
name: "HandleToolErrors",
wrapToolCall: async (request, handler) => {
try {
return await handler(request);
} catch (error) {
// Devolver mensaje de error al modelo para que reintente
return new ToolMessage({
content: `Error en tool: ${error}. Por favor revisa los parámetros.`,
tool_call_id: request.toolCall.id!,
});
}
},
});
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [databaseTool, apiTool],
middleware: [handleToolErrors],
});
```
</v-click>
</div>
+51
View File
@@ -0,0 +1,51 @@
---
layout: default
---
# Middleware Built-in: Resumen
<div class="mt-6 text-xs space-y-3">
<v-click>
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-2">
<code class="font-bold">summarizationMiddleware</code> - Resumir historial cuando supera tokens
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-2">
<code class="font-bold">humanInTheLoopMiddleware</code> - Pausar para aprobación humana
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-2">
<code class="font-bold">toolCallLimitMiddleware</code> - Limitar llamadas a tools
</div>
</v-click>
<v-click>
<div class="border-l-4 border-orange-500 pl-4 bg-orange-50 dark:bg-orange-900/20 p-2">
<code class="font-bold">modelCallLimitMiddleware</code> - Limitar llamadas al modelo
</div>
</v-click>
<v-click>
<div class="border-l-4 border-red-500 pl-4 bg-red-50 dark:bg-red-900/20 p-2">
<code class="font-bold">modelFallbackMiddleware</code> - Fallback a modelos alternativos
</div>
</v-click>
<v-click>
<div class="border-l-4 border-cyan-500 pl-4 bg-cyan-50 dark:bg-cyan-900/20 p-2">
<code class="font-bold">modelRetryMiddleware</code> - Reintentar con backoff exponencial
</div>
</v-click>
<v-click>
<div class="border-l-4 border-yellow-500 pl-4 bg-yellow-50 dark:bg-yellow-900/20 p-2">
<code class="font-bold">piiRedactionMiddleware</code> - Redactar información personal
</div>
</v-click>
</div>
+46
View File
@@ -0,0 +1,46 @@
---
layout: default
---
# Combinando Middleware
<div class="mt-4 text-xs">
<v-click>
```typescript
import {
createAgent,
summarizationMiddleware,
humanInTheLoopMiddleware,
toolCallLimitMiddleware,
modelRetryMiddleware,
} from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model: "mistral:mistral-large-latest",
tools: [searchTool, sendEmailTool, databaseTool],
checkpointer: new MemorySaver(),
middleware: [
modelRetryMiddleware({ maxRetries: 3, backoffFactor: 2 }),
summarizationMiddleware({ model: "mistral:mistral-small-latest", trigger: { tokens: 4000 } }),
toolCallLimitMiddleware({ toolName: "search", runLimit: 5 }),
humanInTheLoopMiddleware({
interruptOn: { send_email: { allowedDecisions: ["approve", "reject"] } },
}),
],
});
```
</v-click>
<v-click>
<div class="mt-3 text-xs opacity-80">
El orden de los middleware importa: se ejecutan en secuencia
</div>
</v-click>
</div>
+49
View File
@@ -0,0 +1,49 @@
---
layout: default
---
<h1 class="!text-2xl">Human-in-the-Loop</h1>
<div class="mt-1">
<v-click>
````md magic-move
```typescript
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { Command, MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model: "gpt-4.1",
tools: [sendEmailTool, deleteRecordTool],
checkpointer: new MemorySaver(),
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
send_email: { allowedDecisions: ["approve", "edit", "reject"] },
delete_record: { allowedDecisions: ["approve", "reject"] },
},
}),
],
});
```
```typescript
const config = { configurable: { thread_id: "thread-1" } };
// Se pausa antes de ejecutar tools sensibles
let result = await agent.invoke({ messages: [...] }, config);
// Aprobar y continuar
result = await agent.invoke(
new Command({ resume: { decisions: [{ type: "approve" }] } }),
config
);
```
````
</v-click>
</div>
---
+51
View File
@@ -0,0 +1,51 @@
---
layout: default
---
# Resumen: Agentes LangChain v1
<div class="mt-6 text-xs space-y-3">
<v-click>
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-2">
<code class="font-bold">createAgent</code> - Punto de entrada principal
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-2">
<code class="font-bold">model</code> - String ("mistral:mistral-large-latest") o instancia ChatModel
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-2">
<code class="font-bold">systemPrompt</code> - String o SystemMessage
</div>
</v-click>
<v-click>
<div class="border-l-4 border-orange-500 pl-4 bg-orange-50 dark:bg-orange-900/20 p-2">
<code class="font-bold">tools</code> - Array de tools definidas con Zod
</div>
</v-click>
<v-click>
<div class="border-l-4 border-red-500 pl-4 bg-red-50 dark:bg-red-900/20 p-2">
<code class="font-bold">checkpointer</code> - MemorySaver | SqliteSaver | PostgresSaver
</div>
</v-click>
<v-click>
<div class="border-l-4 border-cyan-500 pl-4 bg-cyan-50 dark:bg-cyan-900/20 p-2">
<code class="font-bold">middleware</code> - Array para customización (prompts, límites, HITL)
</div>
</v-click>
<v-click>
<div class="border-l-4 border-yellow-500 pl-4 bg-yellow-50 dark:bg-yellow-900/20 p-2">
<code class="font-bold">stream()</code> - updates | values | messages
</div>
</v-click>
</div>
+20
View File
@@ -0,0 +1,20 @@
---
layout: cover
class: text-center
---
# Alternativas en el Ecosistema JS/TS
Frameworks y SDKs para aplicaciones LLM en TypeScript
<div class="pt-12">
<span class="text-xl opacity-80">
Pinceladas rápidas del mercado actual (Mayo 2026)
</span>
</div>
<div class="mt-8 text-sm opacity-60">
Nota: Nos centramos exclusivamente en opciones JavaScript/TypeScript
</div>
+31
View File
@@ -0,0 +1,31 @@
---
layout: default
---
# El Panorama JS/TS para LLMs
<v-clicks>
### Tres niveles de abstracción
```
┌─────────────────────────────────────────────────────┐
│ Frameworks (LangChain, LlamaIndex) │ ← Mayor abstracción
├─────────────────────────────────────────────────────┤
│ SDKs unificados (Vercel AI SDK) │ ← Abstracción media
├─────────────────────────────────────────────────────┤
│ APIs nativas (OpenAI SDK, Anthropic SDK, etc.) │ ← Control total
└─────────────────────────────────────────────────────┘
```
### La pregunta no es "cuál es mejor"
Es: **¿qué nivel de abstracción necesito?**
- **Prototipo rápido** → API nativa o Vercel AI
- **RAG complejo** → LlamaIndex o LangChain
- **Agentes sofisticados** → LangGraph / DeepAgents o Vercel AI SDK 6
- **UI streaming en React** → Vercel AI SDK
</v-clicks>
+50
View File
@@ -0,0 +1,50 @@
---
layout: default
---
# LlamaIndex.TS
El "hermano pequeño" de LlamaIndex (Python)
<v-clicks>
### Realidad del ecosistema TS
<div class="grid grid-cols-2 gap-4">
<div>
**Lo bueno**
- Especializado en RAG
- API similar a la versión Python
- Conectores de datos incluidos
- Activamente desarrollado
</div>
<div>
**La realidad**
- Mejorando rápido (agentes, LiteParse)
- Documentación aún por detrás de Python
- Comunidad más pequeña
- Features llegan después
</div>
</div>
### Ejemplo básico
```typescript
import { Document, VectorStoreIndex } from "llamaindex";
const doc = new Document({ text: "Contenido..." });
const index = await VectorStoreIndex.fromDocuments([doc]);
const response = await index.asQueryEngine().query({ query: "Mi pregunta" });
```
<div class="p-3 bg-yellow-500/10 rounded text-sm">
⚠️ Ha mejorado mucho, pero si tu proyecto permite Python, LlamaIndex Python sigue siendo más completo.
</div>
</v-clicks>
+36
View File
@@ -0,0 +1,36 @@
---
layout: default
---
# Vercel AI SDK
El SDK moderno para aplicaciones AI en JS/TS
<v-clicks>
### Por qué destaca
- **Provider-agnostic**: OpenAI, Anthropic, Google, Mistral, Groq...
- **Streaming nativo**: Pensado para UIs reactivas
- **React-first**: Hooks listos para usar (`useChat`, `useCompletion`)
- **Edge-ready**: Funciona en Vercel, Cloudflare Workers, etc.
- **Ligero**: Pocas abstracciones, código directo
### Ya en v6 — El SDK que más ha crecido
```typescript
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
const { text } = await generateText({
model: openai('gpt-4.1'),
prompt: 'Explica qué es RAG',
});
```
<div class="mt-4 p-3 bg-green-500/10 rounded">
✅ Excelente para: Apps Next.js/React, APIs serverless, prototipos rápidos
</div>
</v-clicks>
+40
View File
@@ -0,0 +1,40 @@
---
layout: default
---
# Vercel AI SDK 6 - Agentes
<v-clicks>
### Agent class (nuevo en v6)
```typescript
import { openai } from '@ai-sdk/openai';
import { Agent, tool } from 'ai';
import { z } from 'zod';
const agent = new Agent({
model: openai('gpt-4.1'),
tools: {
weather: tool({
description: 'Obtiene el clima',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => ({ temp: 22, condition: 'Soleado' }),
}),
},
maxSteps: 5,
});
```
### ¿Agentes completos?
| Tiene ✅ | No tiene ❌ |
|----------|-------------|
| Agent class reutilizable | Persistencia de estado (threads) |
| Human-in-the-loop (`needsApproval`) | Tools built-in (code interpreter) |
| Tools custom + Streaming | Orquestación multi-agente nativa |
</v-clicks>
+52
View File
@@ -0,0 +1,52 @@
---
layout: default
---
# APIs Nativas de los Providers
Cada provider tiene su SDK oficial para Node.js/TS
<v-clicks>
<div class="grid grid-cols-2 gap-6 mt-4">
<div>
### OpenAI SDK
```typescript
import OpenAI from "openai";
const openai = new OpenAI();
// Function calling nativo
const response = await openai.chat.completions.create({
model: "gpt-4.1",
messages: [...],
tools: [{ type: "function", function: {...} }],
});
```
- Assistants API (agentes con estado)
- File search, code interpreter built-in
</div>
<div>
### Anthropic SDK + Agent SDK
```typescript
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const message = await anthropic.messages.create({
model: "claude-sonnet-4-6",
messages: [...],
tools: [{ name: "...", input_schema: {...} }],
});
```
- Tool use muy potente
- **Claude Agent SDK** (`@anthropic-ai/claude-agent-sdk`)
</div>
</div>
</v-clicks>
+42
View File
@@ -0,0 +1,42 @@
---
layout: default
---
# ¿Agentes con APIs Nativas?
Cada provider tiene su propio enfoque
<v-clicks>
### OpenAI: Assistants API
```typescript
const assistant = await openai.beta.assistants.create({
name: "Analyst", tools: [{ type: "code_interpreter" }], model: "gpt-4.1",
});
const thread = await openai.beta.threads.create();
const run = await openai.beta.threads.runs.create(thread.id, { assistant_id: assistant.id });
```
- Agentes con estado, tools built-in (code interpreter, file search)
### Anthropic: Claude Agent SDK
- Agent SDK con tools built-in (Bash, Read, Edit, Web...)
- O tool use manual si prefieres control total
### Google (Gemini): @google/genai SDK
```typescript
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const response = await ai.models.generateContent({
model: "gemini-2.5-flash", contents: "Explica RAG",
});
```
- Function calling similar a OpenAI
- Vertex AI Agents Builder para low-code enterprise
</v-clicks>
+16
View File
@@ -0,0 +1,16 @@
---
layout: default
---
# Comparativa Rápida: Ecosistema JS/TS
| | LangChain.js | LlamaIndex.TS | Vercel AI | Mastra | APIs Nativas |
|---|:---:|:---:|:---:|:---:|:---:|
| **Madurez TS** | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **RAG** | ✅ Completo | ✅ Especializado | ⚠️ Básico | ✅ Integrado | 🔧 Manual |
| **Agentes** | ✅ LangGraph | ⚠️ Básico | ✅ Agent class | ✅✅ Nativo | 🔧 Varía |
| **Streaming UI** | ✅ | ⚠️ | ✅✅ | ⚠️ | ✅ |
| **Curva aprendizaje** | Media-Alta | Media | Baja | Media | Baja |
| **Vendor lock-in** | Bajo | Bajo | Bajo | Bajo | Alto |
+25
View File
@@ -0,0 +1,25 @@
---
layout: default
---
# Recomendación Pragmática
<v-clicks>
| Caso de uso | Recomendación |
|-------------|---------------|
| **Prototipo/MVP** | Vercel AI SDK o API nativa |
| **RAG** | LangChain.js (o Python si puedes) |
| **Agentes complejos** | LangGraph / DeepAgents |
| **App React con chat** | Vercel AI SDK |
| **Control total** | APIs nativas del provider |
<div class="p-3 bg-blue-500/10 rounded mt-2 text-sm">
1. **Empieza simple** → API nativa o Vercel AI
2. **Añade complejidad cuando la necesites** → LangChain/LangGraph
3. **No sobre-ingenierices** → La herramienta mínima que resuelva el problema
</div>
</v-clicks>
+31
View File
@@ -0,0 +1,31 @@
---
layout: default
---
# Reflexión Final
<v-clicks>
### El ecosistema JS/TS para LLMs está madurando
- **2023**: LangChain dominaba, pocas alternativas
- **2024**: Explosión de opciones, Vercel AI despega
- **2025**: Vercel AI SDK 5/6, Claude Agent SDK, DeepAgents
- **2026**: Ecosistema TS ya compite seriamente con Python
### Pero seamos honestos...
<div class="p-4 bg-yellow-500/10 rounded mt-4">
**Python sigue adelante** en el ecosistema LLM:
- Más ejemplos, tutoriales, comunidad
- Features nuevas llegan antes
- Mejor soporte de ML/embeddings locales
Si tu stack lo permite, Python puede ser más productivo para AI.
</div>
</v-clicks>
+38
View File
@@ -0,0 +1,38 @@
---
layout: default
---
# ¿Cuándo JS/TS es viable para AI?
<v-clicks>
### Casos donde tiene sentido
- Tu equipo es **JS/TS puro** y no quieres añadir Python
- Necesitas **edge computing** (Cloudflare Workers, Vercel Edge)
- Tu aplicación es **web-first** con mucho frontend
- Quieres un **stack unificado** (mismo lenguaje front y back)
### El ecosistema está mejorando
<div class="grid grid-cols-2 gap-4 mt-4">
<div class="p-3 bg-green-500/10 rounded">
**Vercel AI SDK** está muy maduro y bien mantenido
</div>
<div class="p-3 bg-green-500/10 rounded">
**LangChain.js** sigue de cerca a Python
</div>
</div>
<div class="mt-6 p-4 bg-blue-500/10 rounded">
**Conclusión**: JS/TS ya es una opción seria para aplicaciones LLM en producción, especialmente si tu contexto lo favorece.
</div>
</v-clicks>
+52
View File
@@ -0,0 +1,52 @@
---
layout: default
---
# Mastra
Framework TypeScript-first para agentes e IA
<v-clicks>
### ¿Qué es?
Framework open-source construido desde cero en TypeScript para crear agentes, workflows y aplicaciones LLM en producción.
<div class="grid grid-cols-2 gap-4 mt-2">
<div>
**Lo que aporta**
- Agentes con memoria, tools y RAG integrado
- Workflows graph-based (nodos + edges tipados)
- Observabilidad con OpenTelemetry incluida
- Integraciones con +100 servicios (GitHub, Slack, Stripe…)
- Syncs para sincronizar datos externos al contexto
</div>
<div>
**Ejemplo básico**
```typescript
import { Mastra, Agent } from "@mastra/core";
const agent = new Agent({
name: "Assistant",
model: openai("gpt-4.1"),
instructions: "Eres un asistente útil.",
tools: { weatherTool },
memory: new Memory(),
});
const mastra = new Mastra({ agents: { agent } });
```
</div>
</div>
<div class="mt-3 p-3 bg-blue-500/10 rounded text-sm">
✅ Buena opción cuando necesitas agentes complejos con memoria, RAG y workflows todo en TypeScript.
[mastra.ai](https://mastra.ai)
</div>
</v-clicks>
+30
View File
@@ -0,0 +1,30 @@
---
layout: center
class: text-center
---
# Eskerrik asko! 🎉
<div class="mt-12 text-xl">
Gracias por vuestra atención
</div>
<div class="mt-10 text-base opacity-80">
¿Dudas o consultas?
</div>
<div class="mt-4 text-lg">
📧 **jabi@nykk.io**
</div>
<div class="mt-10 text-sm opacity-60">
LangChain.js: De cero a producción
</div>
+15
View File
@@ -0,0 +1,15 @@
---
layout: cover
class: text-center
---
# ¿Qué es LangChain?
Framework para desarrollar aplicaciones con Modelos de Lenguaje (LLMs)
<div class="pt-12">
<span class="px-2 py-1 rounded cursor-pointer opacity-80">
Conectando LLMs con el mundo real
</span>
</div>
+33
View File
@@ -0,0 +1,33 @@
---
layout: default
---
# Historia de LangChain
<div class="text-sm">
<v-clicks>
<div class="mb-3">
<p><strong>Octubre 2022</strong>: Nace LangChain en <strong>Python</strong> 🐍</p>
<p class="text-xs opacity-80 ml-4">Creado por Harrison Chase • Enfoque inicial: encadenar prompts y LLMs</p>
</div>
<div class="mb-3">
<p><strong>Diciembre 2022</strong>: Lanzamiento de <strong>LangChain.js</strong> ⚡</p>
<p class="text-xs opacity-80 ml-4">Apenas 2 meses después • Port oficial • Para desarrolladores web</p>
</div>
<div class="mb-3">
<p><strong>2023</strong>: Explosión de adopción en ambos ecosistemas</p>
<p class="text-xs opacity-80 ml-4">Múltiples proveedores de LLMs • 110K+ stars en GitHub</p>
</div>
<div class="mb-3">
<p><strong>2024-2025</strong>: Maduración hacia producción</p>
<p class="text-xs opacity-80 ml-4"><strong>LangGraph</strong>: Control de bajo nivel • <strong>LangSmith</strong>: Observabilidad y testing</p>
</div>
</v-clicks>
</div>
+34
View File
@@ -0,0 +1,34 @@
---
layout: center
class: text-center
---
# Filosofía de LangChain
<div class="grid grid-cols-2 gap-4 mt-8">
<div v-click class="border border-main rounded p-4">
<div class="text-2xl mb-2">🔌</div>
<div class="font-bold">Flexibilidad</div>
<div class="text-sm opacity-80">Sin vendor lock-in</div>
</div>
<div v-click class="border border-main rounded p-4">
<div class="text-2xl mb-2">🔗</div>
<div class="font-bold">Integración</div>
<div class="text-sm opacity-80">LLMs + Datos externos</div>
</div>
<div v-click class="border border-main rounded p-4">
<div class="text-2xl mb-2">🤖</div>
<div class="font-bold">Agentes</div>
<div class="text-sm opacity-80">Decisiones autónomas</div>
</div>
<div v-click class="border border-main rounded p-4">
<div class="text-2xl mb-2">🏗️</div>
<div class="font-bold">Producción</div>
<div class="text-sm opacity-80">Del prototipo a la realidad</div>
</div>
</div>
+33
View File
@@ -0,0 +1,33 @@
---
layout: default
---
# Principios Fundamentales
<div class="grid grid-cols-1 gap-6 mt-8">
<div class="border-l-4 border-blue-500 pl-4">
<h3 class="text-xl font-bold">1. Flexibilidad de Modelos</h3>
<p class="opacity-80">
Estandarización de inputs/outputs entre proveedores de LLMs.
Cambia fácilmente entre OpenAI, Anthropic, Google, etc.
</p>
</div>
<div class="border-l-4 border-green-500 pl-4">
<h3 class="text-xl font-bold">2. Orquestación Compleja</h3>
<p class="opacity-80">
Los modelos coordinan flujos de trabajo con datos externos y herramientas.
Más allá de la simple generación de texto.
</p>
</div>
<div class="border-l-4 border-purple-500 pl-4">
<h3 class="text-xl font-bold">3. Fiabilidad en Producción</h3>
<p class="opacity-80">
Observabilidad, testing y debugging para aplicaciones reales.
El prototipo es fácil, producción es el reto.
</p>
</div>
</div>
+38
View File
@@ -0,0 +1,38 @@
---
layout: default
---
# Creencias Fundamentales
<div class="text-sm">
<v-clicks>
<div class="mb-4">
<h3 class="text-lg">🚀 Los LLMs son tecnología poderosa</h3>
<p class="opacity-80">Una innovación que vale la pena construir sobre ella</p>
</div>
<div class="mb-4">
<h3 class="text-lg">🔗 Los LLMs son mejores con datos externos</h3>
<p class="opacity-80">Combinar LLMs con fuentes de datos externas potencia sus capacidades</p>
</div>
<div class="mb-4">
<h3 class="text-lg">🤖 El futuro es agéntico</h3>
<p class="opacity-80">Las aplicaciones exhibirán capacidades autónomas y de toma de decisiones</p>
</div>
<div class="mb-4">
<h3 class="text-lg">🌱 Estamos en una etapa temprana</h3>
<p class="opacity-80">El campo está en constante evolución con espacio sustancial para crecer</p>
</div>
<div class="mb-4">
<h3 class="text-lg">💪 Producción es difícil</h3>
<p class="opacity-80">Prototipar es fácil, lograr fiabilidad en producción es el verdadero desafío</p>
</div>
</v-clicks>
</div>
+31
View File
@@ -0,0 +1,31 @@
---
layout: center
class: text-center
---
# Ecosistema LangChain
<div class="grid grid-cols-3 gap-8 mt-12">
<div v-click>
<div class="text-4xl mb-4">⛓️</div>
<div class="font-bold text-xl">LangChain (v1)</div>
<div class="text-sm opacity-70 mt-2">Interfaz productiva para agentes</div>
<div class="text-sm opacity-70">createAgent + Middleware</div>
</div>
<div v-click>
<div class="text-4xl mb-4">🕸️</div>
<div class="font-bold text-xl">LangGraph</div>
<div class="text-sm opacity-70 mt-2">Motor de runtime confiable</div>
<div class="text-sm opacity-70">Persistencia, streaming, control granular</div>
</div>
<div v-click>
<div class="text-4xl mb-4">🔍</div>
<div class="font-bold text-xl">LangSmith</div>
<div class="text-sm opacity-70 mt-2">Observabilidad + Evaluación</div>
<div class="text-sm opacity-70">Tracing automático, debugging visual</div>
</div>
</div>
+42
View File
@@ -0,0 +1,42 @@
---
layout: default
---
# ¿Quién usa LangChain?
<div class="grid grid-cols-2 gap-6 mt-6">
<div>
## 🏢 Empresas en Producción
- **LinkedIn** - Agentes con LangGraph
- **Uber** - Sistemas agénticos
- **Replit** - Asistentes de código
- **Klarna** - Chatbots de atención
- **Snowflake** - Integración de datos
- **J.P. Morgan** - Aplicaciones financieras
- **BCG** - Consultoría con IA
</div>
<div>
## 📊 Números de Adopción
- **1,306** empresas verificadas
- **132K+** aplicaciones LLM construidas
- **130M+** descargas totales
- **51%** usando agentes en producción
- **78%** planean usar agentes pronto
### 💼 Casos de Uso Comunes
- Chat sobre documentos
- Atención al cliente
- Asistentes personalizados
- Análisis de datos
</div>
</div>
+52
View File
@@ -0,0 +1,52 @@
---
layout: default
---
# Modelo de Negocio de LangChain
<div class="grid grid-cols-2 gap-6 mt-4 text-sm">
<div>
## 🏢 La Empresa
- **Fundadores**: Harrison Chase y Ankush Gola
- **Fundación**: Octubre 2022
- **Sede**: San Francisco, CA
- **CEO**: Harrison Chase
- **Equipo**: ~136 empleados (2025)
### 💰 Financiación
- **$260M** levantados en total
- **$1.25B** de valoración (Oct 2025)
- **Inversores**: Sequoia, Benchmark, IVP, CapitalG
</div>
<div>
## 💼 Modelo de Negocio
**Freemium B2B**
- Framework open-source (gratis)
- Monetización vía LangSmith
**Revenue 2025**: $16M ARR
- Crecimiento: $8.5M → $16M en 1 año
- **1,000+** clientes empresariales
### 💎 LangSmith Pricing
- **Developer**: Gratis (5k traces/mes)
- **Plus**: $39/usuario/mes (10k traces)
- **Enterprise**: Personalizado + self-hosted
</div>
</div>
+27
View File
@@ -0,0 +1,27 @@
---
layout: two-cols
---
# LangChain.js
La implementación JavaScript/TypeScript del framework LangChain
::right::
<v-clicks>
## ¿Por qué LangChain.js?
- **Desarrollo web nativo**
- Frontend y backend con la misma tecnología
- **Ecosistema JavaScript**
- NPM, TypeScript, Node.js
- **Paridad con Python**
- Mismos conceptos y APIs
- **Comunidad activa**
- Mantenido oficialmente
</v-clicks>
+54
View File
@@ -0,0 +1,54 @@
---
layout: default
---
# LangChain.js: Entornos de Ejecución
<div class="grid grid-cols-2 gap-6 mt-6">
<div>
## 🖥️ Runtimes Soportados
<v-clicks>
- **Node.js** (20.x, 22.x, 24.x)
- ESM y CommonJS
- **Deno** - Runtime seguro
- **Bun** - Runtime ultra-rápido
- **Navegadores** - Client-side
</v-clicks>
</div>
<div>
## ☁️ Edge & Serverless
<v-clicks>
- **Cloudflare Workers**
- **Vercel Edge Functions**
- **Next.js** (Server, Edge, Browser)
- **Supabase Edge Functions**
</v-clicks>
<v-click>
### 💪 Ventaja Clave
**Una sola codebase** para frontend, backend, edge y serverless
</v-click>
</div>
</div>
+59
View File
@@ -0,0 +1,59 @@
---
layout: default
---
# Arquitectura de Componentes
<p class="text-sm opacity-80 -mt-2">Los componentes trabajan juntos para crear aplicaciones AI sofisticadas</p>
<div class="mt-4">
## 🧩 Seis Categorías Principales
<div class="grid grid-cols-3 gap-3 mt-4 text-sm">
<v-click>
<div class="border-l-4 border-blue-500 pl-3 py-1">
<h3 class="font-bold text-base">📝 Models</h3>
<p class="text-xs opacity-80">Chat, LLMs, embeddings</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-3 py-1">
<h3 class="font-bold text-base">🔧 Tools</h3>
<p class="text-xs opacity-80">APIs, búsquedas, DBs</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-3 py-1">
<h3 class="font-bold text-base">🤖 Agents</h3>
<p class="text-xs opacity-80">Orquestación y razonamiento</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-yellow-500 pl-3 py-1">
<h3 class="font-bold text-base">💾 Memory</h3>
<p class="text-xs opacity-80">Historia y estado</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-red-500 pl-3 py-1">
<h3 class="font-bold text-base">🔍 Retrievers</h3>
<p class="text-xs opacity-80">Búsqueda vectorial y web</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-orange-500 pl-3 py-1">
<h3 class="font-bold text-base">📄 Documents</h3>
<p class="text-xs opacity-80">Loaders, splitters</p>
</div>
</v-click>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
---
layout: cover
class: text-center
---
# Módulo 6: LangSmith
Observabilidad, debugging y evaluación
<div class="pt-12">
<span class="text-xl opacity-80">
Llevando tu aplicación a producción
</span>
</div>
+44
View File
@@ -0,0 +1,44 @@
---
layout: default
---
# ¿Qué es LangSmith?
<div class="mt-10 text-sm">
<v-click>
<div class="mb-8">
**LangSmith**: Plataforma de observabilidad y testing para aplicaciones LLM
</div>
</v-click>
<div class="space-y-4 text-xs">
<v-click>
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-3">
<h3 class="text-sm font-bold mb-2">🔍 Observabilidad</h3>
<p>Ver cada paso del agente: prompts, respuestas, tools ejecutadas</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-3">
<h3 class="text-sm font-bold mb-2">🐛 Debugging</h3>
<p>Identificar errores, latencias y costos en producción</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-3">
<h3 class="text-sm font-bold mb-2">✅ Evaluación</h3>
<p>Datasets de prueba y métricas de calidad</p>
</div>
</v-click>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
---
layout: default
---
# ¿Por qué LangSmith?
<div class="mt-10 text-xs space-y-4">
<v-click>
<div class="border-l-4 border-red-500 pl-4 bg-red-50 dark:bg-red-900/20 p-3">
<h3 class="text-sm font-bold mb-2">❌ Sin LangSmith</h3>
<p>• No sabes qué prompts se enviaron exactamente</p>
<p>• Difícil reproducir errores</p>
<p>• Imposible saber el costo real por usuario</p>
<p>• Testing manual y tedioso</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-3">
<h3 class="text-sm font-bold mb-2">✅ Con LangSmith</h3>
<p>• Trazas completas de cada ejecución</p>
<p>• Replay de conversaciones problemáticas</p>
<p>• Dashboard de costos y latencias</p>
<p>• Datasets y evaluaciones automáticas</p>
</div>
</v-click>
</div>
+42
View File
@@ -0,0 +1,42 @@
---
layout: default
---
# Configuración de LangSmith
<div class="mt-8 text-sm">
<v-click>
**Paso 1:** Crear cuenta gratuita en [smith.langchain.com](https://smith.langchain.com)
</v-click>
<v-click>
**Paso 2:** Obtener API Key desde Settings → API Keys
</v-click>
<v-click>
**Paso 3:** Configurar variables de entorno
```bash
# .env
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=lsv2_pt_...
LANGCHAIN_PROJECT=mi-proyecto
```
</v-click>
<v-click>
<div class="mt-6 text-xs opacity-80">
✨ Automático: LangChain detecta las variables y envía trazas a LangSmith
</div>
</v-click>
</div>
+33
View File
@@ -0,0 +1,33 @@
---
layout: default
---
# Ejemplo con LangSmith
<div class="text-xs mt-6">
```typescript
import { createAgent } from "langchain";
// Con las variables de entorno configuradas,
// las trazas se envían automáticamente a LangSmith
const agent = createAgent({
model: "openai:gpt-4.1-mini",
tools: [weatherTool],
});
const result = await agent.invoke({
messages: [{ role: "user", content: "¿Qué tiempo hace en Madrid?" }]
});
// ✅ Ve la traza completa en https://smith.langchain.com
// - Input del usuario
// - Decisión del LLM de usar la tool
// - Ejecución de la tool
// - Respuesta final
// - Tokens consumidos
// - Latencia de cada paso
```
</div>
+37
View File
@@ -0,0 +1,37 @@
---
layout: default
---
# Plan Gratuito
<div class="mt-10 text-sm">
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-4 mb-6">
<h3 class="text-base font-bold mb-3">🎁 Developer (Gratis)</h3>
<p class="text-xs mb-2">• 5,000 traces/mes</p>
<p class="text-xs mb-2">• Retención de 14 días</p>
<p class="text-xs mb-2">• 1 usuario</p>
<p class="text-xs">• Perfecto para desarrollo y testing</p>
</div>
</v-click>
<v-click>
<div class="text-xs opacity-70">
📚 Planes de pago desde $39/mes (Plus) para equipos y producción
</div>
</v-click>
<v-click>
<div class="mt-6 text-center text-sm">
[smith.langchain.com/pricing](https://smith.langchain.com/pricing)
</div>
</v-click>
</div>
+30
View File
@@ -0,0 +1,30 @@
---
layout: default
---
# Próximos Pasos
<div class="mt-10 text-sm space-y-5">
<v-click>
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-4">
<h3 class="text-base font-bold mb-2">🚀 En producción</h3>
<p class="text-xs">Activa LangSmith para monitorear tu aplicación en real-time</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-4">
<h3 class="text-base font-bold mb-2">📚 Explora datasets</h3>
<p class="text-xs">Crea casos de prueba y evalúa mejoras en tus prompts</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-4">
<h3 class="text-base font-bold mb-2">📖 Documentación</h3>
<p class="text-xs">[docs.smith.langchain.com](https://docs.smith.langchain.com)</p>
</div>
</v-click>
</div>
+14
View File
@@ -0,0 +1,14 @@
---
layout: cover
class: text-center
---
# Módulo 4: Mensajes y Prompts
Sistema de mensajes, templates y gestión de contexto
<div class="pt-12">
<span class="text-xl opacity-80">
Comunicándote efectivamente con los LLMs
</span>
</div>
+29
View File
@@ -0,0 +1,29 @@
---
layout: default
---
# Tipos de Mensajes
<div class="mt-4 text-sm">
Los **Chat Models** trabajan con mensajes tipados, no con texto plano. Cada tipo tiene una **carga semántica** que el modelo interpreta de forma diferente
<div class="mt-4 space-y-4">
<v-click>
<div class="border-l-4 border-blue-500 pl-3 bg-blue-50 dark:bg-blue-900/20 py-2 px-3">
<span class="font-bold text-base">💼 SystemMessage</span> <span class="text-xs opacity-70">(role: "system")</span>
<p class="text-sm mt-2 mb-0">Instrucciones iniciales que <b>condicionan todo el comportamiento</b> del modelo. Define persona, reglas, restricciones y formato de respuesta. Tiene la <b>mayor prioridad</b> en la atención del modelo.</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-green-500 pl-3 bg-green-50 dark:bg-green-900/20 py-2 px-3">
<span class="font-bold text-base">👤 HumanMessage</span> <span class="text-xs opacity-70">(role: "user")</span>
<p class="text-sm mt-2 mb-0">Input del usuario. Es lo que el modelo debe <b>responder directamente</b>. Puede contener texto, imágenes, audio y archivos (multimodal).</p>
</div>
</v-click>
</div>
</div>
+33
View File
@@ -0,0 +1,33 @@
---
layout: default
---
# Tipos de Mensajes (cont.)
<div class="mt-4 text-sm">
<div class="mt-4 space-y-4">
<v-click>
<div class="border-l-4 border-purple-500 pl-3 bg-purple-50 dark:bg-purple-900/20 py-2 px-3">
<span class="font-bold text-base">🤖 AIMessage</span> <span class="text-xs opacity-70">(role: "assistant")</span>
<p class="text-sm mt-2 mb-0">Respuesta previa del modelo. Se usa para <b>historial de conversación</b> y para <b>few-shot learning</b>. Puede incluir <code>tool_calls</code>, <code>usage_metadata</code> y <code>response_metadata</code>.</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-orange-500 pl-3 bg-orange-50 dark:bg-orange-900/20 py-2 px-3">
<span class="font-bold text-base">🔧 ToolMessage</span> <span class="text-xs opacity-70">(role: "tool")</span>
<p class="text-sm mt-2 mb-0">Resultado de una herramienta ejecutada. Requiere <code>tool_call_id</code> que enlaza con el <code>AIMessage</code> que solicitó la herramienta. Cierra el <b>ciclo de tool-calling</b>.</p>
</div>
</v-click>
<v-click>
<div class="mt-4 text-xs bg-gray-100 dark:bg-gray-800 rounded p-3">
💡 También puedes usar la <b>notación abreviada</b> con tuplas: <code>["system", "..."]</code>, <code>["human", "..."]</code>, <code>["ai", "..."]</code> dentro de <code>ChatPromptTemplate</code>
</div>
</v-click>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
---
layout: default
---
# Implicaciones: System vs Human
<div class="mt-6 text-sm">
El tipo de mensaje **no es solo una etiqueta**, cambia cómo el modelo procesa la información
<div class="mt-4 grid grid-cols-2 gap-4">
<v-click>
<div class="border rounded-lg p-4 bg-blue-50 dark:bg-blue-900/20">
<h3 class="text-base font-bold mb-2">💼 System vs Human</h3>
<p class="text-xs mb-2">Poner instrucciones en <code>SystemMessage</code> vs en <code>HumanMessage</code> <b>no es equivalente</b>:</p>
<ul class="text-xs list-disc pl-4 space-y-1">
<li>System tiene <b>mayor prioridad</b> — el modelo lo trata como "ley"</li>
<li>Human se trata como "petición" — puede ser ignorado si contradice al system</li>
<li>En Anthropic, system va en un <b>parámetro separado</b> de la API</li>
</ul>
</div>
</v-click>
<v-click>
<div class="border rounded-lg p-4 bg-purple-50 dark:bg-purple-900/20">
<h3 class="text-base font-bold mb-2">🤖 AIMessage manual</h3>
<p class="text-xs mb-2">Puedes <b>inyectar AIMessages falsos</b> en el historial:</p>
<ul class="text-xs list-disc pl-4 space-y-1">
<li>El modelo "cree" que él mismo dijo eso</li>
<li>Útil para <b>few-shot</b>: enseñar el patrón de respuesta deseado</li>
<li>Útil para <b>prefilling</b>: forzar el inicio de la respuesta</li>
</ul>
</div>
</v-click>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
---
layout: default
---
# Implicaciones: Tool Calling y Orden
<div class="mt-6 text-sm">
<div class="mt-4 grid grid-cols-2 gap-4">
<v-click>
<div class="border rounded-lg p-4 bg-orange-50 dark:bg-orange-900/20">
<h3 class="text-base font-bold mb-2">🔧 Ciclo de Tool Calling</h3>
<p class="text-xs mb-2">El flujo completo requiere <b>3 mensajes</b>:</p>
<ol class="text-xs list-decimal pl-4 space-y-1">
<li><code>AIMessage</code> con <code>tool_calls</code> → modelo pide ejecutar</li>
<li><code>ToolMessage</code> con resultado → tu app ejecuta y devuelve</li>
<li><code>AIMessage</code> final → modelo interpreta el resultado</li>
</ol>
<p class="text-xs mt-2 opacity-70">Lo veremos en detalle en el módulo de <b>Agentes</b></p>
</div>
</v-click>
<v-click>
<div class="border rounded-lg p-4 bg-green-50 dark:bg-green-900/20">
<h3 class="text-base font-bold mb-2">📋 El orden importa</h3>
<p class="text-xs mb-2">La <b>secuencia</b> de mensajes define el contexto:</p>
<ul class="text-xs list-disc pl-4 space-y-1">
<li>Los mensajes recientes tienen <b>más peso</b> que los antiguos</li>
<li>El modelo espera alternancia <code>Human → AI → Human</code></li>
<li>Romper la alternancia puede causar comportamiento inesperado</li>
</ul>
<p class="text-xs mt-2 opacity-70">Por eso la <b>gestión del historial</b> es clave</p>
</div>
</v-click>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
---
layout: default
---
# Ejemplo: Usando Mensajes
<div class="text-sm mt-6">
```typescript
import { ChatOpenAI } from "@langchain/openai";
import {
SystemMessage,
HumanMessage
} from "@langchain/core/messages";
const model = new ChatOpenAI({ model: "gpt-4.1-mini" });
const messages = [
new SystemMessage("Eres un experto en JavaScript y TypeScript"),
new HumanMessage("¿Qué es async/await?")
];
const response = await model.invoke(messages);
console.log(response.content);
// Respuesta contextualizada como experto en JS/TS
```
</div>
+47
View File
@@ -0,0 +1,47 @@
---
layout: default
---
# Prompt Templates
<div class="mt-4 text-sm">
<v-click>
<div class="mb-4">
**ChatPromptTemplate** permite reutilizar prompts con variables dinámicas y la notación abreviada de roles
</div>
</v-click>
<v-click>
```typescript
import { ChatPromptTemplate } from "@langchain/core/prompts";
const template = ChatPromptTemplate.fromMessages([
["system", "Eres un experto en {topic}"], // ← shorthand de SystemMessage
["human", "{question}"] // ← shorthand de HumanMessage
]);
const prompt = await template.invoke({
topic: "JavaScript",
question: "¿Qué es async/await?"
});
const response = await model.invoke(prompt);
```
</v-click>
<v-click>
<div class="mt-3 text-xs bg-gray-100 dark:bg-gray-800 rounded p-2">
💡 Los templates son útiles para <b>reutilización</b>, <b>testing</b> y separar prompts de la lógica. Pero para casos simples, puedes usar mensajes directamente.
</div>
</v-click>
</div>
+37
View File
@@ -0,0 +1,37 @@
---
layout: default
---
# Ventajas de Templates
<div class="mt-6 text-sm space-y-3">
<v-click>
<div class="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 py-2 px-3">
<h3 class="text-base font-bold mb-1">✅ Reutilización</h3>
<p class="text-xs">El mismo template con diferentes variables</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 py-2 px-3">
<h3 class="text-base font-bold mb-1">✅ Mantenibilidad</h3>
<p class="text-xs">Cambiar el prompt en un solo lugar</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 py-2 px-3">
<h3 class="text-base font-bold mb-1">✅ Testing</h3>
<p class="text-xs">Probar con múltiples combinaciones de variables</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-orange-500 pl-4 bg-orange-50 dark:bg-orange-900/20 py-2 px-3">
<h3 class="text-base font-bold mb-1">✅ Separación de Responsabilidades</h3>
<p class="text-xs">Prompts independientes de la lógica de negocio</p>
</div>
</v-click>
</div>
+41
View File
@@ -0,0 +1,41 @@
---
layout: default
---
# Few-Shot Prompting
<div class="mt-8 text-sm">
<v-click>
<div class="mb-6">
**Few-shot**: Dar ejemplos al LLM para que aprenda el patrón deseado
</div>
</v-click>
<v-click>
```typescript
import { ChatPromptTemplate } from "@langchain/core/prompts";
const template = ChatPromptTemplate.fromMessages([
["system", "Clasifica el sentimiento de tweets"],
["human", "Me encanta este producto!"],
["ai", "positivo"],
["human", "No funciona bien :("],
["ai", "negativo"],
["human", "{tweet}"] // El tweet a clasificar
]);
const response = await model.invoke(
await template.invoke({ tweet: "Buena experiencia" })
);
// → "positivo"
```
</v-click>
</div>
+40
View File
@@ -0,0 +1,40 @@
---
layout: default
---
# Historial de Conversación
<div class="mt-8 text-sm">
<v-click>
<div class="mb-6">
Para mantener **contexto** entre mensajes, acumula el historial
</div>
</v-click>
<v-click>
```typescript
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
const model = new ChatOpenAI({ model: "gpt-4.1-mini" });
const history = [
new HumanMessage("Hola, me llamo Juan"),
new AIMessage("Hola Juan, ¿en qué puedo ayudarte?"),
new HumanMessage("¿Cuál es mi nombre?")
];
const response = await model.invoke(history);
console.log(response.content);
// → "Tu nombre es Juan"
```
</v-click>
</div>
+44
View File
@@ -0,0 +1,44 @@
---
layout: default
---
# Gestión de Historial
<div class="mt-10 text-sm">
<v-click>
<div class="mb-8">
**Problema**: El historial crece indefinidamente → Tokens infinitos → Costos altos
</div>
</v-click>
<div class="space-y-5">
<v-click>
<div class="border-l-4 border-orange-500 pl-4 bg-orange-50 dark:bg-orange-900/20 p-3">
<h3 class="text-base font-bold mb-2">📏 Limitar por cantidad</h3>
<p class="text-xs">Mantener solo los últimos N mensajes</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-3">
<h3 class="text-base font-bold mb-2">📊 Limitar por tokens</h3>
<p class="text-xs">Mantener solo los últimos N tokens</p>
</div>
</v-click>
<v-click>
<div class="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-3">
<h3 class="text-base font-bold mb-2">📝 Resumir</h3>
<p class="text-xs">Comprimir conversaciones antiguas en un resumen</p>
</div>
</v-click>
</div>
</div>
+34
View File
@@ -0,0 +1,34 @@
---
layout: default
---
# Ejemplo: Limitando Historial
<div class="text-sm mt-6">
```typescript
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
const model = new ChatOpenAI({ model: "gpt-4.1-mini" });
// Simular un historial largo
const fullHistory = [
new HumanMessage("Mensaje 1"),
new AIMessage("Respuesta 1"),
new HumanMessage("Mensaje 2"),
new AIMessage("Respuesta 2"),
new HumanMessage("Mensaje 3"),
new AIMessage("Respuesta 3"),
];
// Mantener solo los últimos 4 mensajes (2 turnos)
const recentHistory = fullHistory.slice(-4);
const response = await model.invoke([
...recentHistory,
new HumanMessage("Nueva pregunta")
]);
```
</div>

Some files were not shown because too many files have changed in this diff Show More