initial commit: slides + practica_resueltos + README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(ls:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../.agents/skills/mcp-builder
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
@@ -0,0 +1 @@
|
||||
v24.16.0
|
||||
@@ -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.
Generated
+4646
File diff suppressed because it is too large
Load Diff
@@ -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 });
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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.
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
@@ -0,0 +1 @@
|
||||
v24.11.1
|
||||
@@ -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
|
||||
```
|
||||
@@ -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>
|
||||
Generated
+9382
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
---
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user