Tutorial: Create an AI agent that reads Wikipedia for you
Tags: ai, gpt3, javascript, beginners
Published: 2023-04-26 12:49:09


In this tutorial, you'll learn how to create an agent that answers questions by searching and reading [Wikipedia](https://www.wikipedia.org/) articles using [JS Agent](https://github.com/lgrammel/js-agent). You can find the complete code in the [Wikipedia agent example](https://github.com/lgrammel/js-agent/tree/main/examples/wikipedia).

This is the high-level flow of the agent:
![Wikipedia agent overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xay324scoavzun5bpbl8.png)

The agent will use the following components:

- OpenAI `gpt-3.5-turbo` chat completion model
- A loop in which the agent determines and executes steps ("GenerateNextStepLoop")
- A custom prompt
- Wikipedia search tool (implemented using a [Programmable Search Engine](https://programmablesearchengine.google.com/))
- Wikipedia article reading tool
- Command line interface and console logger that shows the agent's progress

## Setup

### Pre-requisites

This tutorial assumes that you have Node.js (v18 or newer) installed. You also need access to the OpenAI API.

### Create a new Node.js project

```bash
mkdir wikipedia-agent
cd wikipedia-agent
npm init -y
mkdir src
```

### Setup TypeScript

```bash
npm install --save-dev typescript ts-node @types/node
npx tsc --init --rootDir src --outDir .build
```

### Install JS Agent

```bash
npm install js-agent
```

You should have a basic Node.js project that has TypeScript and JS Agent installed.

### Create agent.ts

`src/agent.ts` will contain the Wikipedia agent. To get started, add the following content:

```typescript
const task = process.argv.slice(2).join(" ");

runWikipediaAgent()
  .then(() => {})
  .catch((error) => {
    console.error(error);
  });

async function runWikipediaAgent() {
  console.log(task);
}
```

You can now run it with e.g.:

```bash
❯ npx ts-node src/agent.ts "how many people live in BC, Canada?"
```

At this point, it only outputs the task to the console.


## Setup the LLM model

Next, we'll set up the LLM model and create the agent loop with a basic prompt.

### Load OpenAI API key

Update the code to load the OpenAI API key from the environment and inject it into the agent:

```typescript
const task = process.argv.slice(2).join(" ");

const openAiApiKey = process.env.OPENAI_API_KEY;
if (!openAiApiKey) {
  throw new Error("OPENAI_API_KEY is not set");
}

runWikipediaAgent()
  .then(() => {})
  .catch((error) => {
    console.error(error);
  });

async function runWikipediaAgent() {
  console.log(openAiApiKey);
  console.log(task);
}
```

## Create the LLM model

First, import JS Agent:

```typescript
import * as $ from "js-agent";
```

You can then create the chat model in `runWikipediaAgent`:

```typescript
const chatGpt = $.provider.openai.chatModel({
  apiKey: openAiApiKey,
  model: "gpt-3.5-turbo",
});
```

### Call the model with a basic prompt

Once you have a model, you can call it directly with a basic prompt:

```typescript
const fullResponse = await chatGpt.generate([
  { role: "user" as const, content: task },
]);
```

And extract the main output from its response:

```typescript
const output = await chatGpt.extractOutput(fullResponse);

console.log(output);
```

Putting this together, this is the current code:

```typescript
import * as $ from "js-agent";

const task = process.argv.slice(2).join(" ");

const openAiApiKey = process.env.OPENAI_API_KEY;
if (!openAiApiKey) {
  throw new Error("OPENAI_API_KEY is not set");
}

runWikipediaAgent()
  .then(() => {})
  .catch((error) => {
    console.error(error);
  });

async function runWikipediaAgent() {
  const chatGpt = $.provider.openai.chatModel({
    apiKey: openAiApiKey,
    model: "gpt-3.5-turbo",
  });

  const fullResponse = await chatGpt.generate([
    { role: "user" as const, content: task },
  ]);

  const output = await chatGpt.extractOutput(fullResponse);

  console.log(task);
  console.log(output);
}
```

When you run it, it'll use the knowledge that's trained into the LLM to answer the question:

```bash
❯ npx ts-node src/agent.ts "how many people live in BC, Canada?"
how many people live in BC, Canada?
As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million.
```

## Create the basic agent

Now we're ready to create a basic agent. Let's update the `runWikipediaAgent` function with the following code:

```typescript
const chatGpt = $.provider.openai.chatModel({
  apiKey: openAiApiKey,
  model: "gpt-3.5-turbo",
});

return $.runAgent<{ task: string }>({
  properties: { task },
  agent: $.step.generateNextStepLoop({
    actions: [],
    actionFormat: $.action.format.flexibleJson(),
    prompt: async ({ runState: { task } }) => [
      { role: "user" as const, content: `${task}` },
    ],
    model: chatGpt,
  }),
  controller: $.agent.controller.maxSteps(3),
  observer: $.agent.observer.showRunInConsole({
    name: "Wikipedia Agent",
  }),
});
```

When you run it, it'll output the basic LLM answer to the console until the maximum number of steps is reached:

```bash
❯ npx ts-node src/agent.ts "how many people live in BC, Canada?"
### Wikipedia Agent ###
{ task: 'how many people live in BC, Canada?' }

Thinking…
As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million.

Thinking…
As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million.

Thinking…
As an AI language model, I do not have access to real-time data. However, according to the latest census conducted in 2016, the population of British Columbia, Canada was approximately 4.6 million.

Cancelled: Maximum number of steps (3) exceeded.
```

Let's dig into the code.

`$.runAgent` runs an agent. It is typed to the properties of the agent, which are also its input. We pass in the `task` as a property:

```typescript
return $.runAgent<{ task: string }>({
  properties: { task },
  // ...
});
```

The `agent` property contains the root step of the agent. We use a `$.step.generateNextStepLoop` step, which generates steps using the LLM until the agent is done:

```typescript
return $.runAgent<...>({
  // ...
  agent: $.step.generateNextStepLoop({
    actions: [],
    actionFormat: $.action.format.flexibleJson(),
    prompt: async ({ runState: { task } }) => [
      { role: "user" as const, content: `${task}` },
    ],
    model: chatGpt,
  }),
```

The loop is configured with our earlier prompt function and the `chatGpt` model. This prompt is used when calling the `chatGpt` model generate function. We'll configure and talk about the actions later.

Because the agent has no actions yet and does not know when to stop, we limit the maximum number of steps to 3:

```typescript
return $.runAgent<...>({
  // ...
  controller: $.agent.controller.maxSteps(3),
```

And finally, we use an observer that outputs the agent's run to the console:

```typescript
return $.runAgent<...>({
  // ...
  observer: $.agent.observer.showRunInConsole({
    name: "Wikipedia Agent",
  }),
```

With the basic agent in place, let's add some tools next.

## Create the Wikipedia search tool

### Create a programmable search engine

First, you need to create a [programmable search engine](https://programmablesearchengine.google.com/about/) for Wikipedia.

When you set up the search engine, configure the site to be `en.wikipedia.org/*`. The search engine id (the `cx` parameter) is on the overview page. You can get the [search engine key in the documentation](https://developers.google.com/custom-search/v1/introduction) ("Get a Key"; requires a Google project).

### Create the search action

JS Agent has a built-in tool for using programmable search engines. You can use it to create a search action.

Tools are actions that run (potentially external) code in some fashion. They don't affect the control flow directly. There are other kinds of actions, e.g., the "done" action, that an agent can select.

```typescript
const searchWikipediaAction = $.tool.programmableGoogleSearchEngineAction({
  id: "search-wikipedia",
  description: "Search wikipedia using a search term. Returns a list of pages.",
  execute: $.tool.executeProgrammableGoogleSearchEngineAction({
    key: "your search engine key",
    cx: "your search engine id",
  }),
});
```

The `id` and `description` parameters are included in the LLM prompt. It is important to choose names and descriptions that are easy to understand for the LLM, because they will determine if and when the agent decides to use this action.

The `execution` parameter contains the function that is running the tool code. It provides additional flexibility, e.g., using executors that run in a Docker container.

## Create an article reader

The read article action is implemented using the JS Agent `extractInformationFromWebpage` tool:

```typescript
const readWikipediaArticleAction = $.tool.extractInformationFromWebpage({
  id: "read-wikipedia-article",
  description:
    "Read a wikipedia article and summarize it considering the query.",
  inputExample: {
    url: "https://en.wikipedia.org/wiki/Artificial_intelligence",
    topic: "{query that you are answering}",
  },
  execute: $.tool.executeExtractInformationFromWebpage({
    extract: $.text.extractRecursively({
      split: $.text.splitRecursivelyAtCharacter({
        maxCharactersPerChunk: 2048 * 4,
      }),
      extract: $.text.generateText({
        id: "summarize-wikipedia-article-chunk",
        prompt: $.prompt.extractChatPrompt(),
        model: chatGpt,
      }),
    }),
  }),
});
```

In addition to the `id` and the `description`, the action has an `inputExample` that will be shown to the LLM. Input examples help with guiding the LLM to take the right action. Every tool has a default input example that can be overridden.

The page is then summarized using text extraction. It is split recursively until the chunks are small enough for `gpt-3.5-turbo` to handle. `gpt-3.5-turbo` is used to generate a summary for each chunk and the concatenated summaries.

`$.prompt.extractChatPrompt()`, which is part of JS Agent, contains the following prompt:

```typescript
async ({ text, topic }: { text: string; topic: string }) => [
  {
    role: "user" as const,
    content: `## TOPIC\n${topic}`,
  },
  {
    role: "system" as const,
    content: `## ROLE
You are an expert at extracting information.
You need to extract and keep all the information on the topic above topic from the text below.
Only include information that is directly relevant for the topic.`,
  },
  {
    role: "user" as const,
    content: `## TEXT\n${text}`,
  },
];
```

Now that we have created a summarization tool, we can put everything together and craft a better agent prompt.

## Adding the actions to the agent

Now that we have a search Wikipedia and a read article action, we can set up the actions in the Wikipedia agent. Let's add them to the actions section of `$.step.generateNextStepLoop`:

```typescript
return $.runAgent<...>({
  // ...
  agent: $.step.generateNextStepLoop({
    actions: [searchWikipediaAction, readWikipediaArticleAction],
    actionFormat: $.action.format.flexibleJson(),
```

The `actionFormat` parses the first flat JSON object in the LLM output. It is specifically designed for ChatGPT, which tends to output the JSON object in various places in the response.

## Create a better prompt

The agent has yet to be made aware of the actions and it does not know that it should read Wikipedia articles. Let's improve the agent prompt.

```typescript
return $.runAgent<...>({
  // ...
  prompt: $.prompt.concatChatPrompts(
    async ({ runState: { task } }) => [
      {
        role: "system",
        content: `## ROLE
You are an knowledge worker that answers questions using Wikipedia content.

## CONSTRAINTS
All facts for your answer must be from Wikipedia articles that you have read.

## TASK
${task}`,
     },
    ],
    $.prompt.availableActionsChatPrompt(),
    $.prompt.recentStepsChatPrompt({ maxSteps: 6 })
  ),
```

Let's dissect the prompt. We first tell the model about its general role, and then we instruct it always to read Wikipedia articles to find the answer and give it the task.

```
## ROLE
You are an knowledge worker that answers questions using Wikipedia content.

## CONSTRAINTS
All facts for your answer must be from Wikipedia articles that you have read.

## TASK
${task}
```

The next part informs the model about the available actions. After that, we ensure to include the last steps the model took in the prompt for the next iteration. This provides some basic memory required for moving the agent forward in its task.

```typescript
return $.runAgent<...>({
  // ...
  prompt: $.prompt.concatChatPrompts(
    // ...
    $.prompt.availableActionsChatPrompt(),
    $.prompt.recentStepsChatPrompt({ maxSteps: 6 })
  ),
```

The different prompts are concatenated using `$.prompt.concatChatPrompts`.

## Update the maximum steps

The agent is now ready to be run.
We increase the maximum number of steps to 10 to provide the agent with more time to find the answer.

```typescript
return $.runAgent<...>({
  // ...
  controller: $.agent.controller.maxSteps(10),
```

## The complete Wikipedia Agent

The agent now looks like this:

```typescript
import * as $ from "js-agent";

const task = process.argv.slice(2).join(" ");

const openAiApiKey = process.env.OPENAI_API_KEY;
if (!openAiApiKey) {
  throw new Error("OPENAI_API_KEY is not set");
}

runWikipediaAgent({
  task,
  openAiApiKey,
})
  .then(() => {})
  .catch((error) => {
    console.error(error);
  });

async function runWikipediaAgent({
  task,
  openAiApiKey,
}: {
  task: string;
  openAiApiKey: string;
}) {
  const chatGpt = $.provider.openai.chatModel({
    apiKey: openAiApiKey,
    model: "gpt-3.5-turbo",
  });

  const searchWikipediaAction = $.tool.programmableGoogleSearchEngineAction({
    id: "search-wikipedia",
    description:
      "Search wikipedia using a search term. Returns a list of pages.",
    execute: $.tool.executeProgrammableGoogleSearchEngineAction({
      key: "your search engine key",
      cx: "your search engine id",
    }),
  });

  const readWikipediaArticleAction = $.tool.extractInformationFromWebpage({
    id: "read-wikipedia-article",
    description:
      "Read a wikipedia article and summarize it considering the query.",
    inputExample: {
      url: "https://en.wikipedia.org/wiki/Artificial_intelligence",
      topic: "{query that you are answering}",
    },
    execute: $.tool.executeExtractInformationFromWebpage({
      extract: $.text.extractRecursively({
        split: $.text.splitRecursivelyAtCharacter({
          maxCharactersPerChunk: 2048 * 4,
        }),
        extract: $.text.generateText({
          id: "summarize-wikipedia-article-chunk",
          prompt: $.prompt.extractChatPrompt(),
          model: chatGpt,
        }),
      }),
    }),
  });

  return $.runAgent<{ task: string }>({
    properties: { task },
    agent: $.step.generateNextStepLoop({
      actions: [searchWikipediaAction, readWikipediaArticleAction],
      actionFormat: $.action.format.flexibleJson(),
      prompt: $.prompt.concatChatPrompts(
        async ({ runState: { task } }) => [
          {
            role: "system",
            content: `## ROLE
You are an knowledge worker that answers questions using Wikipedia content.

## CONSTRAINTS
All facts for your answer must be from Wikipedia articles that you have read.

## TASK
${task}`,
          },
        ],
        $.prompt.availableActionsChatPrompt(),
        $.prompt.recentStepsChatPrompt({ maxSteps: 6 })
      ),
      model: chatGpt,
    }),
    controller: $.agent.controller.maxSteps(10),
    observer: $.agent.observer.showRunInConsole({ name: "Wikipedia Agent" }),
  });
}
```

## Example Run

Here are the results of a test run:

```
❯ npx ts-node src/agent.ts "how many people live in BC, Canada?"
### Wikipedia Agent ###
{ task: 'how many people live in BC, Canada?' }

Thinking…
I will start by searching for the population of British Columbia, Canada on Wikipedia.

{
  "action": "search-wikipedia",
  "query": "population of british columbia canada"
}

Executing search-wikipedia…
## Found 5 search results.
### British Columbia - Wikipedia
https://en.wikipedia.org/wiki/British_Columbia
With an estimated population of 5.3 million as of 2022, it is Canada's third-most populous province. The capital of British Columbia is Victoria and its ...

### Vancouver - Wikipedia
https://en.wikipedia.org/wiki/Vancouver
Vancouver is a major city in western Canada, located in the Lower Mainland region of British Columbia. ... Vancouver has the highest population density in Canada, with over 5,700 ...

### Demographics of British Columbia - Wikipedia
https://en.wikipedia.org/wiki/Demographics_of_British_Columbia
Population of British Columbia 5.399 million (2023) ; Percentage of National Population: 13.2% (unchanged) ; Population Growth Rate: 5.6% ...

### Victoria, British Columbia - Wikipedia
https://en.wikipedia.org/wiki/Victoria,_British_Columbia
Victoria is the capital city of the Canadian province of British Columbia, on the southern ... In the 2021 Census of Population conducted by Statistics Canada, ...

### Surrey, British Columbia - Wikipedia
https://en.wikipedia.org/wiki/Surrey,_British_Columbia
In the 2021 Canadian census conducted by Statistics Canada, Surrey had a population of 568,322 living in 185,671 of its 195,098 total private dwellings, a ...


Thinking…
The third search result, "Demographics of British Columbia - Wikipedia", seems to be the most relevant. I will read the article and summarize it to answer the question.

{
  "action": "read-wikipedia-article",
  "url": "https://en.wikipedia.org/wiki/Demographics_of_British_Columbia",
  "topic": "population of British Columbia, Canada"
}

Executing read-wikipedia-article…
## Extracted information on topic 'population of British Columbia, Canada' from https://en.wikipedia.org/wiki/Demographics_of_British_Columbia
The population of British Columbia, Canada is 5.399 million as of 2023, which represents 13.2% of the national population. The population growth rate is 5.6%. In terms of visible minorities and indigenous peoples, 30.3% of the population in 2016 were visible minorities, and 5.9% were indigenous. The largest visible minority groups were South Asian, Chinese, and Black. In terms of immigration, in 2021, immigrants comprised 29.0% of the total population of British Columbia, with the largest immigrant groups being from China, India, and the Philippines. Finally, the 2021 Canadian census counted a total of 197,420 people who immigrated to British Columbia between 2016 and 2021.

Thinking…
According to the information I found on Wikipedia, the population of British Columbia, Canada is 5.399 million as of 2023.

{
  "action": "done"
}

Done
```