You can integrate MongoDB Atlas with LangGraph.js to build AI agents. This tutorial demonstrates how to build an agent with LangGraph.js and MongoDB Vector Search that can answer questions about your data.
具体的には、次のアクションを実行します。
環境を設定します。
MongoDBクラスターを構成します。
エージェントツールを含むエージェントをビルドします。
エージェントにメモリを追加します。
サーバーを作成し、エージェントをテストします。
このチュートリアルのコードでは、 GitHubリポジトリをクローンします。
前提条件
始める前に、以下のものを必ず用意してください。
次のいずれか 1 つ。
MongoDBバージョン 6.0.11を実行中Atlas クラスター7.0.2、またはそれ以降IPアドレスが Atlas プロジェクトのアクセス リストに含まれていることを確認します。
Atlas CLI を使用して作成されたローカル Atlas 配置。詳細については、「 Atlas クラスターのローカル配置 」を参照してください。
投票AI APIキー。アカウントとAPIキーを作成するには、 Vyage AI のウェブサイト を参照してください。
OpenAI APIキー。 APIリクエストに使用できるクレジットを持つ OpenAI アカウントが必要です。 OpenAI アカウントの登録の詳細については、 OpenAI APIウェブサイト を参照してください。
注意
このチュートリアルでは OpenAI と Vyage AIのモデルを使用していますが、コードを変更して、お好みのモデルを使用することができます。
環境設定
環境を設定するには、以下の手順を完了します。
プロジェクトを初期化し、依存関係をインストールします。
新しいプロジェクトディレクトリを作成し、プロジェクト内で次のコマンドを実行して必要な依存関係をインストールします。
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
注意
プロジェクトでは、次の構造を使用します。
├── .env ├── index.ts ├── agent.ts ├── seed-database.ts ├── package.json ├── tsconfig.json
環境ファイルを作成します。
プロジェクトルートに .env
ファイルを作成し、 APIキーと接続文字列を追加します。
OPENAI_API_KEY = "<openai-api-key>" MONGODB_URI = "<connection-string>" VOYAGEAI_API_KEY = "<voyage-api-key>"
<connection-string>
を Atlas クラスターまたはローカル Atlas 配置の接続文字列に置き換えます。
接続stringには、次の形式を使用する必要があります。
mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net
接続stringには、次の形式を使用する必要があります。
mongodb://localhost:<port-number>/?directConnection=true
詳細については、「接続文字列 」を参照してください。
MongoDBクラスターを構成する
このセクションでは、サンプルデータを構成してMongoDBクラスターに取り込み、データに対してベクトル検索を有効にします。
MongoDB .に接続するためのファイルを作成します。
MongoDBクラスターへの接続を確立する index.ts
ファイルを作成します。
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();
サンプルデータをクラスターに投入します。
サンプルの従業員レコードを生成して格納するseed-database.ts
スクリプトを作成してください。このスクリプトは、次のアクションを実行します。
従業員レコードのスキーマを定義します。
LM を使用してサンプル従業員データを生成する関数を作成します。
各レコードを処理して、埋め込みに使用するテキストのサマリーを作成します。
LgChuin MongoDB統合を使用して、 MongoDBクラスターをベクトルストアとして初期化します。このコンポーネントはベクトル埋め込みを生成し、ドキュメントを
hr_database.employees
名前空間に保存します。
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://api.voyageai.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);
シードスクリプトを実行します。
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
スクリプトを実行中後、Atlas UIの 名前空間に移動すると、 MongoDBクラスター内のシード データを表示できます。hr_database.employees
Create an MongoDB Vector Search index.
Follow the steps to create an MongoDB Vector Search index for the hr_database.employees
namespace. Name the index vector_index
and specify the following index definition:
{ "fields": [ { "numDimensions": 1024, "path": "embedding", "similarity": "cosine", "type": "vector" } ] }
エージェントをビルドする
このセクションでは、エージェントのワークフローをオーケストレーションするためのグラフを構築します。グラフは、エージェントがクエリに応答するための手順の順序を定義します。
agent.ts
ファイルを作成します。
プロジェクトに agent.ts
という名前の新しいファイルを作成し、次のコードを追加してエージェントの設定を開始してください。次の手順では、非同期関数にさらにコードを追加されます。
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) }
エージェントの状態を定義します。
グラフの状態を定義するには、ファイルに次のコードを追加します。
const GraphState = Annotation.Root({ messages: Annotation<BaseMessage[]>({ reducer: (x, y) => x.concat(y), }), });
状態は、エージェントのワークフローを通過するデータ構造を定義します。ここでは、 状態は、新しいメッセージを既存の対話履歴に連結する削減値を使用して、対話メッセージを追跡します。
ツールを定義します。
Add the following code to define a tool and tool node that uses MongoDB Vector Search to retrieve relevant employee information by querying the 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://api.voyageai.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);
追加の関数を定義します。
次のコードを追加して、エージェントがメッセージを処理し、通信を継続するかどうかを判断するために使用する関数を定義します。
この関数は、エージェントがLM を使用する方法を構成します。
システム指示とやり取り履歴を含むプロンプト テンプレートを作成します。
現在の時刻、使用可能なツール、メッセージを含むプロンプトの形式を設定します。
LLM を呼び出して、次の応答を生成します。
会話状態に追加するモデルの応答を返します。
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] }; } この関数は、エージェントが会話を続けるべきか、終了するべきかを決定します。
メッセージにツール呼び出しが含まれている場合、フローをツールノードにルーティングします。
それ以外の場合は、対話を終了し、最終応答を返します。
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__"; }
エージェントのワークフローを定義します。
以下のコードを追加して、エージェントがクエリに応答するための一連のステップを定義します。
const workflow = new StateGraph(GraphState) .addNode("agent", callModel) .addNode("tools", toolNode) .addEdge("__start__", "agent") .addConditionalEdges("agent", shouldContinue) .addEdge("tools", "agent");
具体的に言うと、エージェントは以下の手順を実行します。
エージェントは、ユーザーのクエリを受け取ります。
エージェントノードでは、エージェントがクエリを処理し、ツールを使用するか、会話を終了するかを決定します。
ツールが必要な場合、エージェントはツールノードにルーティングし、選択されたツールを実行します。ツールの結果はエージェントノードに返送されます。
エージェントはツールの出力を解釈し、応答を形成するか、次のアクションを決定します。
エージェントがこれ以上の処理は不要であると判断するまでこれが続きます(
shouldContinue
関数がend
を返すとき)。
このエージェントには、2つのカスタムノードを定義します。
エージェントノード: このノードは現在の状態のメッセージを処理し、これらのメッセージを使用して LM を呼び出し、すべてのツール呼び出しを含む LM の応答で状態をアップデートします。
ツール ノード: このノードはツール呼び出しを処理し、現在の状態に基づいて使用する適切なツールを決定し、ツール呼び出しの結果で会話履歴を更新します。
また、エッジを定義して、グラフ内のノードを接続し、エージェントのフローを定義します。このコードでは、次のエッジを定義します。
次の通常のエッジは、以下のルーティングを行います。
開始ノードからエージェント ノードへ。
エージェント ノードからツールノードへ。
エージェントノードの出力に基づいてフローをルーティングする条件付きエッジ。エージェントノードがツールを必要としていると判断した場合は、 ツールノードにルーティングします。そうでない場合、交信を終了します。
エージェントにメモリを追加します
MongoDB チェックポインターを使用することで、エージェントの状態を永続化し、パフォーマンスを向上させることができます。永続性により、エージェントは以前のインタラクションに関する情報を保存できます。エージェントはこれを今後のインタラクションで使用し、より文脈的に関連性の高い応答を提供できます。
エージェント機能を完了します。
最後に、次のコードを追加して、クエリを処理するエージェント関数を完了します。
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;
サーバーを作成してエージェントをテストします
このセクションでは、エージェントと交流し、その機能をテストするためのサーバーを作成します。
Express.jsサーバーを構成します。
index.ts
ファイルを次のコードで置き換えてください。
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();
エージェントをテストします。
エージェントとやり取りするためのサンプルリクエストを送信してください。使用するデータとモデルによって、応答内容は異なります。
注意
リクエストはJSON形式で応答を返します。サーバーが稼働しているターミナルで、プレーンテキストの出力を表示することもできます。
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?
前の応答で返されたスレッド ID を使用して、会話を続行することができます。例えば、フォローアップの質問をするには、次のコマンドを使用してください。<threadId>
を前の応答で返されたスレッド ID に置き換えてください。
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.