DEV Community

Cover image for Creating a ReAct AI Agent with nodeJS ( search wikipedia )
Allan Felipe Murara
Allan Felipe Murara

Posted on

Creating a ReAct AI Agent with nodeJS ( search wikipedia )

Introduction

We'll create an AI agent capable of searching Wikipedia and answering questions based on the information it finds. This ReAct (Reason and Act) Agent uses the Google Generative AI API to process queries and generate responses. Our agent will be able to:

  1. Search Wikipedia for relevant information.
  2. Extract specific sections from Wikipedia pages.
  3. Reason about the information gathered and formulate answers.

[2] What is a ReAct Agent?

A ReAct Agent is a specific type of agent that follows a Reflection-Action cycle. It reflects on the current task, based on available information and actions it can perform, and then decides which action to take or whether to conclude the task.

[3] Planning the Agent

3.1 Required Tools

  • Node.js
  • Axios library for HTTP requests
  • Google Generative AI API (gemini-1.5-flash)
  • Wikipedia API

3.2 Agent Structure

Our ReAct Agent will have three main states:

  1. THOUGHT (Reflection)
  2. ACTION (Execution)
  3. ANSWER (Response)

3.3 Thought State

The thought state is the moment where the ReactAgent will reflect over the collected information and decide what should be the next step.

async thought() {
 .....
}
Enter fullscreen mode Exit fullscreen mode

3.4 Action State (ACTION)

In the action state, the agent executes one of the available functions based on the previous Thought.
Note that there is the action ( execution ) and the decision ( which action ).

async action() {
 // call the decision 
 // execute the action and return a ActionResult
}

async decideAction() {
 // Call the llm based on the Thought ( reflection ) to format and adequate the functionCall.
// Look around for a function-tool mode at [google dapi docs](https://ai.google.dev/gemini-api/docs/function-calling)
}
Enter fullscreen mode Exit fullscreen mode

[4] Implementing the Agent

Let's build the ReAct Agent step by step, highlighting each state.

4.1 Initial Setup

First, set up the project and install dependencies:

mkdir react-agent-project
cd react-agent-project
npm init -y
npm install axios dotenv @google/generative-ai
Enter fullscreen mode Exit fullscreen mode

Create a .env file at the project's root:

GOOGLE_AI_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

FREE ApiKey here

4.2 Function Declaration

This file is the Javascript file that NodeJS will use for actually executing an api call to Wikipedia.
We describe the content of this file at FunctionDescription.

Create Tools.js with the following content:

const axios = require("axios");

class Tools {
  static async wikipedia(q) {
    try {
      const response = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          list: "search",
          srsearch: q,
          srwhat: "text",
          format: "json",
          srlimit: 4,
        },
      });

      const results = await Promise.all(
        response.data.query.search.map(async (searchResult) => {
          const sectionResponse = await axios.get(
            "https://en.wikipedia.org/w/api.php",
            {
              params: {
                action: "parse",
                pageid: searchResult.pageid,
                prop: "sections",
                format: "json",
              },
            },
          );

          const sections = Object.values(
            sectionResponse.data.parse.sections,
          ).map((section) => `${section.index}, ${section.line}`);

          return {
            pageTitle: searchResult.title,
            snippet: searchResult.snippet,
            pageId: searchResult.pageid,
            sections: sections,
          };
        }),
      );

      return results
        .map(
          (result) =>
            `Snippet: ${result.snippet}\nPageId: ${result.pageId}\nSections: ${JSON.stringify(result.sections)}`,
        )
        .join("\n\n");
    } catch (error) {
      console.error("Error fetching from Wikipedia:", error);
      return "Error fetching data from Wikipedia";
    }
  }

  static async wikipedia_with_pageId(pageId, sectionId) {
    if (sectionId) {
      const response = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "parse",
          format: "json",
          pageid: parseInt(pageId),
          prop: "wikitext",
          section: parseInt(sectionId),
          disabletoc: 1,
        },
      });
      return Object.values(response.data.parse?.wikitext ?? {})[0]?.substring(
        0,
        25000,
      );
    } else {
      const response = await axios.get("https://en.wikipedia.org/w/api.php", {
        params: {
          action: "query",
          pageids: parseInt(pageId),
          prop: "extracts",
          exintro: true,
          explaintext: true,
          format: "json",
        },
      });
      return Object.values(response.data?.query.pages)[0]?.extract;
    }
  }
}

module.exports = Tools;
Enter fullscreen mode Exit fullscreen mode

4.3 Creating the ReactAgent.js File

Create ReactAgent.js with the following content:

require("dotenv").config();
const { GoogleGenerativeAI } = require("@google/generative-ai");
const Tools = require("./Tools");

const genAI = new GoogleGenerativeAI(process.env.GOOGLE_AI_API_KEY);

class ReActAgent {
  constructor(query, functions) {
    this.query = query;
    this.functions = new Set(functions);
    this.state = "THOUGHT";
    this._history = [];
    this.model = genAI.getGenerativeModel({
      model: "gemini-1.5-flash",
      temperature: 2,
    });
  }

  get history() {
    return this._history;
  }

  pushHistory(value) {
    this._history.push(`\n ${value}`);
  }

  async run() {
    this.pushHistory(`**Task: ${this.query} **`);
    try {
      return await this.step();
    } catch (e) {
      if (e.message.includes("exhausted")) {
        return "Sorry, I'm exhausted, I can't process your request anymore. ><";
      }
      return "Unable to process your request, please try again? ><";
    }
  }

  async step() {
    const colors = {
      reset: "\x1b[0m",
      yellow: "\x1b[33m",
      red: "\x1b[31m",
      cyan: "\x1b[36m",
    };

    console.log("====================================");
    console.log(
      `Next Movement: ${
        this.state === "THOUGHT"
          ? colors.yellow
          : this.state === "ACTION"
            ? colors.red
            : this.state === "ANSWER"
              ? colors.cyan
              : colors.reset
      }${this.state}${colors.reset}`,
    );
    console.log(`Last Movement: ${this.history[this.history.length - 1]}`);
    console.log("====================================");
    switch (this.state) {
      case "THOUGHT":
        return await this.thought();
        break;
      case "ACTION":
        return await this.action();
        break;
      case "ANSWER":
        return await this.answer();
        break;
    }
  }

  async promptModel(prompt) {
    const result = await this.model.generateContent(prompt);
    const response = await result.response;
    return response.text();
  }


  async thought() {
    const availableFunctions = JSON.stringify(Array.from(this.functions));
    const historyContext = this.history.join("\n");

//Play around with the prompt to perceive the differences.
//Feel free to comment asking for guidance if anything.
//Improving the thought part can be as simple as adding two lines to the beginning of the prompt, finally becoming :  

//Your task to FullFill ${this.query}.
//If you already have an answer for the task, you can go ahead and FullFill it.
//Otherwise you act accordingly to the following scenario >>>


const prompt = `Your task to FullFill ${this.query}.
Context contains all the reflection you made so far and the ActionResult you collected.
AvailableActions are functions you can call whenever you need more data.

Context: "${historyContext}" <<

AvailableActions: "${availableFunctions}" <<

Task: "${this.query}" <<

Reflect uppon Your Task using Context, ActionResult and AvailableActions to find your next_step.
print your next_step with a Thought or FullFill Your Task `;

    const thought = await this.promptModel(prompt);
    this.pushHistory(`\n **${thought.trim()}**`);
    if (
      thought.toLowerCase().includes("fullfill") ||
      thought.toLowerCase().includes("fulfill")
    ) {
      this.state = "ANSWER";
      return await this.step();
    }
    this.state = "ACTION";
    return await this.step();
  }

  async action() {
    const action = await this.decideAction();
    this.pushHistory(`** Action: ${action} **`);
    const result = await this.executeFunctionCall(action);
    this.pushHistory(`** ActionResult: ${result} **`);
    this.state = "THOUGHT";
    return await this.step();
  }

  async decideAction() {
    const availableFunctions = JSON.stringify(Array.from(this.functions));
    const historyContext = this.history;
    const prompt = `Reflect uppon the Thought, Query and AvailableActions

    ${historyContext[historyContext.length - 2]}

    Thought <<< ${historyContext[historyContext.length - 1]}

    Query: "${this.query}"

    AvailableActions: ${availableFunctions}

    output only the function,parametervalues separated by a comma. For example: "wikipedia,ronaldinho gaucho, 1450"`;

    const decision = await this.promptModel(prompt);
    return `${decision.replace(/`/g, "").trim()}`;
  }

  async executeFunctionCall(functionCall) {
    const [functionName, ...args] = functionCall.split(",");
    const func = Tools[functionName.trim()];
    if (func) {
      return await func.call(null, ...args);
    }
    throw new Error(`Function ${functionName} not found`);
  }

  async answer() {
    const historyContext = this.history;
    const prompt = `Based on the following context, provide a complete, detailed and descriptive formated answer for the Following Task: ${this.query} .

Context:
${historyContext}

Task: "${this.query}"`;

    const finalAnswer = await this.promptModel(prompt);
    this.history.push(`Answer: ${this.finalAnswer}`);
    return finalAnswer;
  }
}

module.exports = ReActAgent;
Enter fullscreen mode Exit fullscreen mode

4.4 Running the agent and explaining the available tools (index.js)

Create index.js with the following content:

const ReActAgent = require("./ReactAgent.js");

async function main() {
  const query = "What does England border with?";
  // const query = "what are the neighbourhoods of Joinville?";
  // const query = "what is the capital of France?";
  // const query = "What does england borders with?";
  // const query = "what is the color of the sky?";
  const functions = [
    [
      "wikipedia",
      "params: query",
      "Semantic Search Wikipedia API for snippets, pageIds and sectionIds >> \n ex: Date brazil has been colonized? \n Brazil was colonized at 1500, pageId, sections : []",
    ],
    [
      "wikipedia_with_pageId",
      "params : pageId, sectionId",
      "Search Wikipedia API for data using a pageId and a sectionIndex as params.  \n ex: 1500, 1234 \n Section information about blablalbal",
    ],
  ];

  const agent = new ReActAgent(query, functions);
  try {
    const result = await agent.run();
    console.log("THE AGENT RETURN THE FOLLOWING >>>", result);
  } catch (e) {
    console.log("FAILED TO RUN T.T", e);
  }
}

main().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Function description

Whenever trying to add a new tool or function make sure to describe it well.
At our example, this is already done, and added to our class ReActAgent while calling a new Instance.

const functions = [
    [
      "google", //functionName
      "params: query", //LocalParamsName
      "Semantic Search Wikipedia API for snippets, pageIds and sectionIds >> \n ex: Date brazil has been colonized? \n Brazil was colonized at 1500, pageId, sections : []", // briefly explanation and example (this will be forward to the LLM)
    ]
Enter fullscreen mode Exit fullscreen mode

[5] How the Wikipedia Part Works

The interaction with Wikipedia is done in two main steps:

  1. Initial search (wikipedia function):

    • Makes a request to the Wikipedia search API.
    • Returns up to 4 relevant results for the query.
    • For each result, it fetches the sections of the page.
  2. Detailed search (wikipedia_with_pageId function):

    • Uses the page ID and section ID to fetch specific content.
    • Returns the text of the requested section.

This process allows the agent to first get an overview of topics related to the query and then dive deeper into specific sections as needed.

[6] Execution Flow Example

  1. The user asks a question.
  2. The agent enters the THOUGHT state and reflects on the question.
  3. It decides to search Wikipedia and enters the ACTION state.
  4. Executes the wikipedia function and obtains results.
  5. Returns to the THOUGHT state to reflect on the results.
  6. May decide to search for more details or a different approach.
  7. Repeats the THOUGHT and ACTION cycle as necessary.
  8. When it has sufficient information, it enters the ANSWER state.
  9. Generates a final answer based on all the information collected.
  10. Enters infinite loop whenever the wikipedia doesn't have the data to collect. Fix it with a timer =P

[7] Final Considerations

  • The modular structure allows for easy addition of new tools or APIs.
  • It's important to implement error handling and time/iteration limits to avoid infinite loops or excessive resource use.
  • This example uses temperature 2. The smaller the temperature, the less creative during turns this agent becomes. Play around with it to perceive the influence of temperature in llms.

Top comments (0)