Logo
Tutorials/Agent with RAG

2. Create a basic agent

We want to use await so we're going to wrap all of our code in a main function, like this:

// Your imports go here

async function main() {
  // the rest of your code goes here
}

main().catch(console.error);

For the rest of this guide we'll assume your code is wrapped like this so we can use await. You can run the code this way:

npx tsx example.ts

Load your dependencies

First we'll need to pull in our dependencies. These are:

  • The OpenAI class to use the OpenAI LLM
  • tool to provide tools to our agent
  • agent to create the single agent
  • Settings to define some global settings for the library
  • Dotenv to load our API key from the .env file
  • Zod to define the schema for our tool
import "dotenv/config";
import {
  agent,
  agentStreamEvent,
  openai,
} from "@llamaindex/workflow";
import {
  tool,
  Settings,
} from "llamaindex";
import {
  openai,
} from "@llamaindex/openai";
import { z } from "zod";

Initialize your LLM

We need to tell our OpenAI class where its API key is, and which of OpenAI's models to use. We'll be using gpt-4o, which is capable while still being pretty cheap. This is a global setting, so anywhere an LLM is needed will use the same model.

Settings.llm = openai({
  apiKey: process.env.OPENAI_API_KEY,
  model: "gpt-4o",
});

Create a function

We're going to create a very simple function that adds two numbers together. This will be the tool we ask our agent to use.

const sumNumbers = ({ a, b }) => {
  return `${a + b}`;
};

Note that we're passing in an object with two named parameters, a and b. This is a little unusual, but important for defining a tool that an LLM can use.

Turn the function into a tool for the agent

This is the most complicated part of creating an agent. We need to define a tool. We have to pass in:

  • The function itself (sumNumbers)
  • A name for the function, which the LLM will use to call it
  • A description of the function. The LLM will read this description to figure out what the tool does, and if it needs to call it
  • A schema for function. We tell the LLM that the parameter is an object, and we tell it about the two named parameters we gave it, a and b. We describe each parameter as a number, and we say that both are required.
  • You can see more examples of function schemas.
const addTool = tool({
  name: "sumNumbers",
  description: "Use this function to sum two numbers",
  parameters: z.object({
    a: z.number({
      description: "First number to sum",
    }),
    b: z.number({
      description: "Second number to sum",
    }),
  }),
  execute: sumNumbers,
});

We then wrap up the tools into an array. We could provide lots of tools this way, but for this example we're just using the one.

const tools = [addTool];

Create the agent

With your LLM already set up and your tools defined, creating an agent is simple:

const myAgent = agent({ tools });

Ask the agent a question

We can use the run method to ask our agent a question, and it will use the tools we've defined to find an answer.

const result = await myAgent.run("Sum 101 and 303");
console.log(result.data);

You will see the following output:

Output

{ result: 'The sum of 101 and 303 is 404.' }

To stream the response, you need to call runStream, which returns a stream of events. The agentStreamEvent provides chunks of the response as they become available. This allows you to display the response incrementally rather than waiting for the full response:

const events = myAgent.runStream("Add 101 and 303");
for await (const event of events) {
  if (agentStreamEvent.include(event)) {
    process.stdout.write(event.data.delta);
  }
}

Streaming Output

The sum of 101 and 303 is 404.

Note that we're filtering for agentStreamEvent as an agent might return other events - more about that in the following section.

Logging workflow events

To log the workflow events, you can check the event type and log the event data.

const events = myAgent.runStream("Sum 202 and 404");
for await (const event of events) {
  if (agentStreamEvent.include(event)) {
    // Stream the response
    process.stdout.write(event.data.delta);
  } else {
    // Log other events
    console.log("\nWorkflow event:", JSON.stringify(event, null, 2));
  }
}

Let's see what running this looks like using npx tsx agent.ts

Output

Workflow event: {
  "data": {
    "userInput": "Sum 202 and 404"
  },
  "displayName": "StartEvent"
}

Workflow event: {
  "data": {
    "input": [
      {
        "role": "user",
        "content": "Sum 202 and 404"
      }
    ],
    "currentAgentName": "Agent"
  },
  "displayName": "AgentInput"
}

Workflow event: {
  "data": {
    "input": [
      {
        "role": "system",
        "content": "You are a helpful assistant. Use the provided tools to answer questions."
      },
      {
        "role": "user",
        "content": "Sum 202 and 404"
      }
    ],
    "currentAgentName": "Agent"
  },
  "displayName": "AgentSetup"
}

....

We're seeing several workflow events being logged:

  1. AgentToolCall - Shows the agent preparing to call our tool with the numbers 202 and 404
  2. AgentToolCallResult - Shows the result of calling the tool, which returned "606"
  3. AgentInput - Shows the original user input
  4. AgentOutput - Shows the agent's response

Great! We've built an agent that can understand requests and use tools to fulfill them. Next you can: