Make the MongoDB docs better! We value your opinion. Share your feedback for a chance to win $100.
MongoDB Branding Shape
Click here >
Docs Menu

Compilar un agente de IA con LangGraph.js y MongoDB Atlas

Puedes integrar MongoDB Atlas con LangGraph.js para compilar agentes de IA. Este tutorial demuestra cómo compilar un agente con LangGraph.js y MongoDB Vector Search que pueda responder preguntas sobre tus datos.

En concreto, realizas las siguientes acciones:

  1. Configura el entorno.

  2. Configura tu clúster de MongoDB.

  3. Compile el agente, incluyendo las herramientas del agente.

  4. Añadir memoria al agente.

  5. Crea un servidor y prueba el agente.

Trabaje con el código de este tutorial clonando el repositorio de GitHub.

Antes de comenzar, asegúrate de tener lo siguiente:

Nota

Este tutorial usa modelos de OpenAI e Voyage IA, pero puedes modificar el código para usar el modelo que elijas.

Para configurar el entorno, complete los siguientes pasos:

1

Cree un nuevo directorio de proyecto y luego ejecute los siguientes comandos en el proyecto para instalar las dependencias requeridas:

npm init -y
npm i -D typescript ts-node @types/express @types/node
npx tsc --init
npm i langchain @langchain/langgraph @langchain/mongodb @langchain/community @langchain/langgraph-checkpoint-mongodb dotenv express mongodb zod

Nota

Tu proyecto usa la siguiente estructura:

├── .env
├── index.ts
├── agent.ts
├── seed-database.ts
├── package.json
├── tsconfig.json
2

Crea un archivo .env en la raíz de tu proyecto y añade tus claves API y la cadena de conexión:

OPENAI_API_KEY = "<openai-api-key>"
MONGODB_URI = "<connection-string>"
VOYAGEAI_API_KEY = "<voyage-api-key>"

Se debe sustituir <connection-string> por la cadena de conexión del clúster Atlas o de la implementación local de Atlas.

Su cadena de conexión debe usar el siguiente formato:

mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net

Para obtener más información, consulta Conectar a un clúster a través de bibliotecas de clientes.

Su cadena de conexión debe usar el siguiente formato:

mongodb://localhost:<port-number>/?directConnection=true

Para obtener más información, consulta Cadenas de conexión.

Puede seguir este tutorial viendo el video.

Duración: 30 minutos

En esta sección, se configura y se ingieren datos de muestra en el clúster MongoDB para habilitar búsquedas vectoriales sobre los datos.

1

Cree un archivo index.ts que establezca una conexión con su clúster de MongoDB:

import { MongoClient } from "mongodb";
import 'dotenv/config';
const client = new MongoClient(process.env.MONGODB_URI as string);
async function startServer() {
try {
await client.connect();
await client.db("admin").command({ ping: 1 });
console.log("Pinged your deployment. You successfully connected to MongoDB!");
// ... rest of the server setup
} catch (error) {
console.error("Error connecting to MongoDB:", error);
process.exit(1);
}
}
startServer();
2

Cree un script de seed-database.ts para generar y almacenar registros de empleados de ejemplo. Este script realiza las siguientes acciones:

  • Define un esquema para los registros de empleados.

  • Crea una función para generar datos de muestra de empleados utilizando el LLM.

  • Procesa cada registro para crear un resumen de texto que se utilizará para incrustaciones.

  • Utiliza la integración de MongoDB de LangChain para inicializar su clúster de MongoDB como un vector store. Este componente genera vector embeddings y almacena los documentos en tu hr_database.employees namespace.

Copia y pega el siguiente código en tu archivo seed-base-de-datos.ts.

import { ChatOpenAI } from "@langchain/openai";
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { MongoClient } from "mongodb";
import { z } from "zod";
import "dotenv/config";
const llm = new ChatOpenAI({
modelName: "gpt-4o-mini",
temperature: 0.7,
});
const EmployeeSchema = z.object({
employee_id: z.string(),
first_name: z.string(),
last_name: z.string(),
date_of_birth: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
state: z.string(),
postal_code: z.string(),
country: z.string(),
}),
contact_details: z.object({
email: z.string().email(),
phone_number: z.string(),
}),
job_details: z.object({
job_title: z.string(),
department: z.string(),
hire_date: z.string(),
employment_type: z.string(),
salary: z.number(),
currency: z.string(),
}),
work_location: z.object({
nearest_office: z.string(),
is_remote: z.boolean(),
}),
reporting_manager: z.string().nullable(),
skills: z.array(z.string()),
performance_reviews: z.array(
z.object({
review_date: z.string(),
rating: z.number(),
comments: z.string(),
})
),
benefits: z.object({
health_insurance: z.string(),
retirement_plan: z.string(),
paid_time_off: z.number(),
}),
emergency_contact: z.object({
name: z.string(),
relationship: z.string(),
phone_number: z.string(),
}),
notes: z.string(),
});
type Employee = z.infer<typeof EmployeeSchema>;
const parser = StructuredOutputParser.fromZodSchema(z.array(EmployeeSchema));
async function generateSyntheticData(): Promise<Employee[]> {
const prompt = `You are a helpful assistant that generates employee data. Generate 10 fictional employee records. Each record should include the following fields: employee_id, first_name, last_name, date_of_birth, address, contact_details, job_details, work_location, reporting_manager, skills, performance_reviews, benefits, emergency_contact, notes. Ensure variety in the data and realistic values.
${parser.getFormatInstructions()}`;
console.log("Generating synthetic data...");
const response = await llm.invoke(prompt);
return parser.parse(response.content as string);
}
async function createEmployeeSummary(employee: Employee): Promise<string> {
return new Promise((resolve) => {
const jobDetails = `${employee.job_details.job_title} in ${employee.job_details.department}`;
const skills = employee.skills.join(", ");
const performanceReviews = employee.performance_reviews
.map(
(review) =>
`Rated ${review.rating} on ${review.review_date}: ${review.comments}`
)
.join(" ");
const basicInfo = `${employee.first_name} ${employee.last_name}, born on ${employee.date_of_birth}`;
const workLocation = `Works at ${employee.work_location.nearest_office}, Remote: ${employee.work_location.is_remote}`;
const notes = employee.notes;
const summary = `${basicInfo}. Job: ${jobDetails}. Skills: ${skills}. Reviews: ${performanceReviews}. Location: ${workLocation}. Notes: ${notes}`;
resolve(summary);
});
}
const fetchEmbeddings = async (records: { pageContent: string }[]) => {
const apiUrl = "https://ai.mongodb.com/v1/embeddings";
const apiKey = process.env.VOYAGEAI_API_KEY;
const inputs = records.map(record => record.pageContent);
const requestBody = {
input: inputs,
model: "voyage-3.5",
};
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error while fetching embeddings:", error);
}
};
async function seedDatabase(): Promise<void> {
try {
const client = new MongoClient(process.env.MONGODB_URI as string);
await client.connect();
await client.db("admin").command({ ping: 1 });
console.log("Pinged your deployment. You successfully connected to MongoDB!");
const db = client.db("hr_database");
const collection = db.collection("employees");
await collection.deleteMany({});
const syntheticData = await generateSyntheticData();
const recordsWithSummaries = await Promise.all(
syntheticData.map(async (record) => ({
pageContent: await createEmployeeSummary(record),
metadata: {...record},
}))
);
for (const record of recordsWithSummaries ) {
const db = client.db("hr_database");
const collection = db.collection("employees");
const embedding = await fetchEmbeddings([record]);
const enrichedRecord = {
pageContent: record.pageContent,
metadata: record.metadata,
embedding: embedding.data[0].embedding
}
const result = await collection.insertOne(enrichedRecord);
console.log("Successfully added database record:", result);
}
await client.close();
} catch (error) {
console.error("Error seeding database:", error);
}}
seedDatabase().catch(console.error);
3
npx ts-node seed-database.ts
Pinged your deployment. You successfully connected to MongoDB!
Generating synthetic data...
Successfully added database record: {
acknowledged: true,
insertedId: new ObjectId('685d89d966545cfb242790f0')
}
Successfully added database record: {
acknowledged: true,
insertedId: new ObjectId('685d89d966545cfb242790f1')
}
Successfully added database record: {
acknowledged: true,
insertedId: new ObjectId('685d89da66545cfb242790f2')
}
Successfully added database record: {
acknowledged: true,
insertedId: new ObjectId('685d89da66545cfb242790f3')
}

Tip

Después de ejecutar el script, puedes ver los datos iniciales en tu clúster de MongoDB navegando hasta el namespace hr_database.employees en la Interfaz de usuario de Atlas.

4

Siga los pasos para crear un índice de MongoDB búsqueda vectorial para el namespace hr_database.employees. Asigne un nombre al índice vector_index y especifique la siguiente definición de índice:

{
"fields": [
{
"numDimensions": 1024,
"path": "embedding",
"similarity": "cosine",
"type": "vector"
}
]
}

En esta sección, se construye un grafo para orquestar el flujo de trabajo del agente. El grafo define la secuencia de pasos que sigue el agente para responder a una query.

1

Cree un nuevo archivo llamado agent.ts en su proyecto y luego agregue el siguiente código para comenzar a configurar el agente. Agregarás más código a la función asíncrona en los pasos siguientes.

import { ChatOpenAI } from "@langchain/openai";
import { AIMessage, BaseMessage, HumanMessage } from "@langchain/core/messages";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { StateGraph } from "@langchain/langgraph";
import { Annotation } from "@langchain/langgraph";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { MongoDBSaver } from "@langchain/langgraph-checkpoint-mongodb";
import { MongoDBAtlasVectorSearch } from "@langchain/mongodb";
import { MongoClient } from "mongodb";
import { z } from "zod";
import "dotenv/config";
export async function callAgent(client: MongoClient, query: string, thread_id: string) {
// Define the MongoDB database and collection
const dbName = "hr_database";
const db = client.db(dbName);
const collection = db.collection("employees");
// ... (Add rest of code here)
}
2

Agrega el siguiente código al archivo para definir el grafo estado:

const GraphState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});

El estado define la estructura de datos que circula a través del flujo de trabajo de su agente. Aquí, el estado rastrea los mensajes de conversación, con un reductor que concatena los nuevos mensajes al historial de conversación existente.

3

Añade el siguiente código para definir una herramienta y un nodo de herramienta que utilicen MongoDB Vector Search para recuperar información relevante sobre empleados mediante la consulta al vector store:

const executeQuery = async (embedding:[], n: number) => {
try {
const client = new MongoClient(process.env.MONGODB_URI as string);
const database = client.db("hr_database");
const coll = database.collection("employees");
const agg = [
{
'$vectorSearch': {
'index': 'vector_index',
'path': 'embedding',
'queryVector': embedding,
'numCandidates': 150,
'limit': n
}
}, {
'$project': {
'_id': 0,
'pageContent': 1,
'score': {
'$meta': 'vectorSearchScore'
}
}
}
];
const result = await coll.aggregate(agg).toArray();
return result
} catch(error) {
console.log("Error while querying:", error)
}
}
const fetchEmbeddings = async (query: string) => {
const apiUrl = "https://ai.mongodb.com/v1/embeddings";
const apiKey = process.env.VOYAGEAI_API_KEY;
const requestBody = {
input: query,
model: "voyage-3.5",
};
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.data[0].embedding;
} catch (error) {
console.error("Error while fetching embedding:", error);
}
};
const employeeLookupTool = tool(
async ({ query, n = 10 }) => {
console.log("Employee lookup tool called");
const embedding = await fetchEmbeddings(query)
const response = await executeQuery(embedding, n)
const result = JSON.stringify(response)
return result;
},
{
name: "employee_lookup",
description: "Gathers employee details from the HR database",
schema: z.object({
query: z.string().describe("The search query"),
n: z.number().optional().default(10).describe("Number of results to return"),
}),
}
);
const tools = [employeeLookupTool];
const toolNode = new ToolNode<typeof GraphState.State>(tools);
4

Agrega el siguiente código al archivo para determinar qué modelo usar para el agente. Este ejemplo utiliza un modelo de OpenAI, pero puedes modificarlo para utilizar tu modelo preferido:

const model = new ChatOpenAI({
model: "gpt-4o"
}).bindTools(tools);
5

Agrega el siguiente código para definir las funciones que utiliza el agente para procesar los mensajes y determinar si debe continuar la conversación:

  1. Esta función configura cómo el agente utiliza el LLM:

    • Construye una plantilla de prompt con instrucciones del sistema y el historial de la conversación.

    • Da formato a la indicación con la hora actual, las herramientas disponibles y los mensajes.

    • Invoca el LLM para generar la siguiente respuesta.

    • Devuelve la respuesta del modelo que se añadirá al estado de la conversación.

    async function callModel(state: typeof GraphState.State) {
    const prompt = ChatPromptTemplate.fromMessages([
    [
    "system",
    `You are a helpful AI assistant, collaborating with other assistants. Use the provided tools to progress towards answering the question. If you are unable to fully answer, that's OK, another assistant with different tools will help where you left off. Execute what you can to make progress. If you or any of the other assistants have the final answer or deliverable, prefix your response with FINAL ANSWER so the team knows to stop. You have access to the following tools: {tool_names}.\n{system_message}\nCurrent time: {time}.`,
    ],
    new MessagesPlaceholder("messages"),
    ]);
    const formattedPrompt = await prompt.formatMessages({
    system_message: "You are helpful HR Chatbot Agent.",
    time: new Date().toISOString(),
    tool_names: tools.map((tool) => tool.name).join(", "),
    messages: state.messages,
    });
    const result = await model.invoke(formattedPrompt);
    return { messages: [result] };
    }
  2. Esta función determina si el agente debe continuar o finalizar la conversación:

    • Si el mensaje contiene llamadas a herramientas, dirija el flujo al nodo de herramientas.

    • De lo contrario, finaliza la conversación y devuelve la respuesta final.

    function shouldContinue(state: typeof GraphState.State) {
    const messages = state.messages;
    const lastMessage = messages[messages.length - 1] as AIMessage;
    if (lastMessage.tool_calls?.length) {
    return "tools";
    }
    return "__end__";
    }
6

Agregue el siguiente código para definir la secuencia de pasos que toma el agente para responder a una query.

const workflow = new StateGraph(GraphState)
.addNode("agent", callModel)
.addNode("tools", toolNode)
.addEdge("__start__", "agent")
.addConditionalEdges("agent", shouldContinue)
.addEdge("tools", "agent");

Concretamente, el agente ejecuta los siguientes pasos:

  1. El agente recibe una query.

  2. En el nodo del agente, el agente procesa la query y determina si debe usar una herramienta o finalizar la conversación.

  3. Si se necesita una herramienta, el agente enruta al nodo de herramientas, donde ejecuta la herramienta seleccionada. El resultado de la herramienta se envía de vuelta al nodo del agente.

  4. El agente interpreta el resultado de la herramienta y forma una respuesta o decide la siguiente acción.

  5. Esto continúa hasta que el agente determine que no se necesita ninguna acción adicional (la función shouldContinue devuelve end).

Diagrama que muestra el flujo de trabajo del agente LangGraph-MongoDB.

Expanda esta sección para aprender más sobre nodos y aristas.

Para este agente, define dos nodos personalizados:

  • Nodo de agente: Este nodo procesa los mensajes en el estado actual, invoca el modelo de lenguaje (LLM) con estos mensajes y actualiza el estado con la respuesta del LLM, la cual incluye cualquier llamada a herramientas.

  • Nodo de herramientas: Este nodo procesa las llamadas a herramientas, determina la herramienta adecuada para usar según el estado actual y actualiza el historial de la conversación con los resultados de la llamada a la herramienta.

También se definen los bordes para conectar los nodos en el grafo y definir el flujo del agente. En este código, defines las siguientes edges:

  • Los siguientes bordes normales esa ruta:

    • Inicia el nodo al nodo agente.

    • Nodo agente a nodo de herramientas.

  • Un borde condicional que enruta el flujo basado en la salida del nodo agente. Si el nodo agente determina que se necesita una herramienta, enruta hacia el nodo de herramientas. De lo contrario, finaliza la conversación.

Para mejorar el rendimiento del agente, puedes persistir su estado utilizando el Checkpointer de MongoDB. La persistencia permite al agente almacenar información sobre interacciones anteriores, que éste puede utilizar en interacciones futuras para brindar respuestas más contextualmente relevantes.

1

Agrega el siguiente código a tu archivo agent.ts para configurar una capa de persistencia para el estado de tu agente:

const checkpointer = new MongoDBSaver({ client, dbName });
const app = workflow.compile({ checkpointer });
2

Por último, agrega el siguiente código para completar la función del agente que gestiona las queries:

const finalState = await app.invoke(
{
messages: [new HumanMessage(query)],
},
{ recursionLimit: 15, configurable: { thread_id: thread_id } }
);
console.log(finalState.messages[finalState.messages.length - 1].content);
return finalState.messages[finalState.messages.length - 1].content;

En esta sección, crearás un servidor para interactuar con tu agente y probar su funcionalidad.

1

Reemplaza tu archivo index.ts con el siguiente código:

import 'dotenv/config';
import express, { Express, Request, Response } from "express";
import { MongoClient } from "mongodb";
import { callAgent } from './agent';
const app: Express = express();
app.use(express.json());
const client = new MongoClient(process.env.MONGODB_URI as string);
async function startServer() {
try {
await client.connect();
await client.db("admin").command({ ping: 1 });
console.log("Pinged your deployment. You successfully connected to MongoDB!");
app.get('/', (req: Request, res: Response) => {
res.send('LangGraph Agent Server');
});
app.post('/chat', async (req: Request, res: Response) => {
const initialMessage = req.body.message;
const threadId = Date.now().toString();
try {
const response = await callAgent(client, initialMessage, threadId);
res.json({ threadId, response });
} catch (error) {
console.error('Error starting conversation:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/chat/:threadId', async (req: Request, res: Response) => {
const { threadId } = req.params;
const { message } = req.body;
try {
const response = await callAgent(client, message, threadId);
res.json({ response });
} catch (error) {
console.error('Error in chat:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (error) {
console.error('Error connecting to MongoDB:', error);
process.exit(1);
}
}
startServer();
2

Ejecuta el siguiente comando para iniciar tu servidor:

npx ts-node index.ts
3

Envía solicitudes de muestra para interactuar con tu agente. Tus respuestas varían en función de tus datos y de los modelos que utilices.

Nota

La solicitud devuelve una respuesta en formato JSON. También se puede ver la salida de texto sin formato en la terminal en el que se ejecuta el servidor.

curl -X POST -H "Content-Type: application/json" -d '{"message": "Build a team to make a web app based on the employee data."}' http://localhost:3000/chat
# Sample response
{"threadId": "1713589087654", "response": "To assemble a web app development team, we ideally need..." (truncated)}
# Plaintext output in the terminal
To assemble a web app development team, we ideally need the following roles:
1. **Software Developer**: To handle the coding and backend.
2. **UI/UX Designer**: To design the application's interface and user experience.
3. **Data Analyst**: For managing, analyzing, and visualizing data if required for the app.
4. **Project Manager**: To coordinate the project tasks and milestones, often providing communication across departments.
### Suitable Team Members for the Project:
#### 1. Software Developer
- **John Doe**
- **Role**: Software Engineer
- **Skills**: Java, Python, AWS
- **Location**: Los Angeles HQ (Remote)
- **Notes**: Highly skilled developer with exceptional reviews (4.8/5), promoted to Senior Engineer in 2018.
#### 2. Data Analyst
- **David Smith**
- **Role**: Data Analyst
- **Skills**: SQL, Tableau, Data Visualization
- **Location**: Denver Office
- **Notes**: Strong technical analysis skills. Can assist with app data integration or dashboards.
#### 3. UI/UX Designer
No specific UI/UX designer was identified in the current search. I will need to query this again or look for a graphic designer with some UI/UX skills.
#### 4. Project Manager
- **Emily Davis**
- **Role**: HR Manager
- **Skills**: Employee Relations, Recruitment, Conflict Resolution
- **Location**: Seattle HQ (Remote)
- **Notes**: Experienced in leadership. Can take on project coordination.
Should I search further for a UI/UX designer, or do you have any other parameters for the team?

Puedes continuar la conversación utilizando el ID del hilo devuelto en tu respuesta anterior. Por ejemplo, para hacer una pregunta de seguimiento, utiliza el siguiente comando. Reemplace <threadId> con el ID del hilo devuelto en la respuesta anterior.

curl -X POST -H "Content-Type: application/json" -d '{"message": "Who should lead this project?"}' http://localhost:3000/chat/<threadId>
# Sample response
{"response": "For leading this project, a suitable choice would be someone..." (truncated)}
# Plaintext output in the terminal
### Best Candidate for Leadership:
- **Emily Davis**:
- **Role**: HR Manager
- **Skills**: Employee Relations, Recruitment, Conflict Resolution
- **Experience**:
- Demonstrated leadership in complex situations, as evidenced by strong performance reviews (4.7/5).
- Mentored junior associates, indicating capability in guiding a team.
- **Advantages**:
- Remote-friendly, enabling flexible communication across team locations.
- Experience in managing people and processes, which would be crucial for coordinating a diverse team.
**Recommendation:** Emily Davis is the best candidate to lead the project given her proven leadership skills and ability to manage collaboration effectively.
Let me know if you'd like me to prepare a structured proposal or explore alternative options.