šŸ“• Build AI Agents using LangGraph.js is now out!

Using ChatOpenAI with LangGraph.js to Build a Personal Assistant AI Agent

Let's see how we can use LangGraph.js to build a personal assistant AI Agent powered by ChatGPT.

This is the second part of an introduction series to LangGraph.js. You can see here the first pare where we go through how to setup nodes, edges, conditional edges, and basic graphs in LangGraph.js.

The AI Agent will help us to make a phone call to someone who lives in another timezone. For example, if we pass it an input like the one below:

"What is the time now in Singapore?"
"I would like to call a friend who lives there."

We will receive an answer like this:

"šŸ¤– The current time in Singapore is 2:30 AM." 
"It might be a bit too early to call your friend."

In this article, we will cover the following topics:

  • defining a graph for an AI Agent using LangGraph.js
  • setting up tools for the AI Agent
  • interpreting the various results using the AI Agent
  • dealing with failing API calls

Setting up the environment

To begin, you'll need to install the necessary libraries. Here's a quick setup:

npm install @langchain/core @langchain/langgraph @langchain/openai zod dotenv

Once installed, load your environment variables using dotenv:

import * as dotenv from "dotenv";
dotenv.config();

Remember that you will also need a .env file to place your OPENAI_API_KEY key:

// .env
OPENAI_API_KEY=sk-1234567890

Initialize ChatOpenAI and define the tools

We'll use a `ChatOpenAI` object from LangChain to drive the AI's conversation. By setting the temperature to 0, the model produces more predictable and deterministic responses:

const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });

Using a tool function, we create a custom tool to check the local time in a specific city. To simulate the unreliability of real-world tools, we randomly introduce failures for every third call.

We define the schema for the tool using the `Zod` library:

const gmtTimeSchema = z.object({
  city: z.string().describe("The name of the city"),
});

const gmtTimeTool = tool(
  async ({ city }) => {
    const serviceIsWorking = Math.floor(Math.random() * 3);
    return serviceIsWorking !== 2
      ? "The local time in " + city  + " is 6:30pm."
      : "Error 404";
 },
 {
    name: "gmtTime",
    description: `Check local time in a specified city. 
 The API is randomly available every third call.`,
    schema: gmtTimeSchema,
 }
);

const toolNode = new ToolNode([gmtTimeTool]);

In this example, the tool provides the current time in a city, but may occasionally fail, simulating an unreliable API.

You can read more about tool calling in LangChain.js here and here.

Defining the StateGraph with LangGrah.js

The `StateGraph` defines how the conversation flows between nodes, or stages, of the conversation. It sets up nodes for invoking the LLM and tool usage, and defines how to switch between them:

const graph = new StateGraph(MessagesAnnotation)
 .addNode("agent", callModel)
 .addNode("tools", toolNode)
 .addEdge(START, "agent")
 .addEdge("tools", "agent")
 .addConditionalEdges("agent", shouldContinue, ["tools", END]);

const runnable = graph.compile();

Here, `addNode()` links the steps in the AI's process, such as interacting with the model `callModel` and the tools `toolNode`.

The `addConditionalEdges()` method ensures the workflow moves forward based on the result of the tool called.

The structure of our Graph looks like so:

You can see this structure by running the following code:

const image = await runnable.getGraph().drawMermaidPng()
const arrayBuffer = await image.arrayBuffer()
await fs.writeFileSync('graph-struct.png', new Uint8Array(arrayBuffer))

Setting LangGraph.js nodes

While the general workflow is defined using a StateGraph object from LangGraph we need to define the content of each node from the graph.

The `shouldContinue()` function is the key component. It determines whether to continue or end the agent execution.

const callModel = async (state) => {
  const { messages } = state;
  const llmWithTools = llm.bindTools([gmtTimeTool]);
  const result = await llmWithTools.invoke(messages);
  return { messages: [result] };
};

const shouldContinue = (state) => {
  const lastMessage = getLastMessage(state);
  const didAICalledAnyTools = lastMessage._getType() === "ai" &&
    lastMessage.tool_calls?.length;
  return didAICalledAnyTools ? "tools" : END;
};

Setting up the AI Agent behavior

Now, we can feed in the messages and invoke the workflow. We set the scene for our AI agent by defining its responsibilities in a `SystemMessage` and then asking it to provide help for making a phone call:

const result = await runnable.invoke({
  messages: [
    new SystemMessage(
      `You are responsible for answering user questions using tools. 
 These tools sometimes fail, but you keep trying until 
 you get a valid response.`
 ),
    new HumanMessage(
      `What is the time now in Singapore? 
I would like to call a friend who lives there.`
 ),
 ]
});

Finally, we log the AI's response to the console:

console.log("šŸ¤–  " + getLastMessage(result).content )

We will get an output like the one below:

"šŸ¤– The current time in Singapore is 6:30 PM." 
"You can go ahead and call your friend!"

One interesting fact about having AI features is that is that if we change the output of the `gmtTimeTool` the model will recommend that it's not a good time to make the phone call:

// updating the output of gmtTimeTool
return "The local in " + city + "time is 2:30am."

// will lead to updating the final ouput
"šŸ¤– The current time in Singapore is 2:30 AM." 
"It might be a bit too early to call your friend."

In case the API call fails we will see a stream of messages similar to the one below:

messages: [
    HumanMessage {
      "content": "What is the time now in Singapore? I would like to call a friend there.",
      // more attributes here
    },
    AIMessage {
      "additional_kwargs": {
        "tool_calls": []
        // more attributes here
    },
    },
    ToolMessage {
      "content": "Error 404",
      "name": "gmtTime",
      // more attributes here
    },
    AIMessage {
      "content": "It seems there was an issue retrieving the current time in Singapore. Let me try again.",
      // more attributes here
    },
    // keeps going until it receives an answer
]

Putting it all together

This is how the final full version of the code will look like:

import { HumanMessage, SystemMessage } from "@langchain/core/messages"
import { ToolNode } from "@langchain/langgraph/prebuilt"
import {
  END, MessagesAnnotation, START, StateGraph
} from "@langchain/langgraph"
import { ChatOpenAI } from "@langchain/openai"
import { tool } from "@langchain/core/tools"
import { z } from "zod"
import * as dotenv from "dotenv"

dotenv.config()

const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 })

const getLastMessage = 
    ({ messages }) => messages[messages.length - 1]

const gmtTimeSchema = z.object({
  city: z.string().describe("The name of the city")
})

const gmtTimeTool = tool(
  async ({ city }) => {
    const serviceIsWorking = Math.floor(Math.random() * 3)
    return serviceIsWorking !== 2
      ? "The local in " + city + " time is 2:30am."
      : "Error 404"
 },
 {
    name: "gmtTime",
    description: `Check local time in a specified city. 
 The API is randomly available every third call.`,
    schema: gmtTimeSchema,
 }
)

const tools = [gmtTimeTool]
const toolNode = new ToolNode(tools)
const llmWithTools = llm.bindTools(tools)

const callModel = async (state) => {
  const { messages } = state
  const result = await llmWithTools.invoke(messages)
  return { messages: [result] }
}

const shouldContinue = (state) => {
  const lastMessage = getLastMessage(state)
  const didAICalledAnyTools = lastMessage._getType() === "ai" &&
    lastMessage.tool_calls?.length
  return didAICalledAnyTools ? "tools" : END
}

const graph = new StateGraph(MessagesAnnotation)
 .addNode("agent", callModel)
 .addNode("tools", toolNode)
 .addEdge(START, "agent")
 .addEdge("tools", "agent")
 .addConditionalEdges("agent", shouldContinue, ["tools", END])

const runnable = graph.compile()

const result = await runnable.invoke({
  messages: [
    new SystemMessage(
      `You are responsible for answering user questions using tools. 
 These tools sometimes fail, but you keep trying until 
 you get a valid response.`
 ),
    new HumanMessage(
        `What is the time now in Singapore?
I would like to call a friend who lives there.`
 ),
 ]
})

console.log("šŸ¤– " + getLastMessage(result).content)

The code we built here showcases a resilient agent that can handle failures and retry operations, simulating real-world scenarios where APIs might occasionally fail. This setup provides a foundation for more complex personal assistants with broader applications.

As usual, this code is also available on my GitHub.

Keep coding, friends, and feel free to leave me a comment if you have any questions.

šŸ“– Build a full trivia game app with LangChain

Learn by doing with this FREE ebook! This 35-page guide walks you through every step of building your first fully functional AI-powered app using JavaScript and LangChain.js

šŸ“– Build a full trivia game app with LangChain

Learn by doing with this FREE ebook! This 35-page guide walks you through every step of building your first fully functional AI-powered app using JavaScript and LangChain.js


Leave a Reply

Your email address will not be published. Required fields are marked *