Agents are an experimental feature in RΞASON. Their API is subject to change.

What is an agent?

There are some different definitions people use to define “LLM agents”. This is confusing.

So we propose the following:

An agent is just a LLM that selects which action to take from predefined pool of actions in order to accomplish a certain objective.

An example

Although people don’t think of it like it, we argue that ChatGPT is indeed an agent. (And probably the most popular in the world)

ChatGPT Plus lets users:

  • chat with GPT-4;
  • create images with DALL·E 3;
  • get relevant answer with GPT-4 web browsing;
  • plot graphs with GPT-4 running Python code and showing its output.

How does ChatGPT (the app, not the LLM) knows which action to take?

Because the LLM itself (GPT-4) decides which action to take — just respond to user, create an image using DALL·E, etc.

Its objective could be: “Your goal is to fulfill the user’s need — be that responding the message, creating a new image, web browsing, etc.”

And the actions available for the agent are:

  • Send text message to user;
  • Create image using DALLE-3;
  • Make a web search and visit the results;
  • Run a piece of python code and get its output.

The point here is not to discuss semantics but rather illustrate what RΞASON refers to as “agents”.

Why are agents useful?

LLMs have strong reasoning capabilities and, because of that they are able to decide when to call an action (and which to call) pretty precisely.

For instance, LLMs are bad at math:

LLM failing at math

But if we give access to a “Sum” action that takes two numbers and outputs the sum of them, the LLM correctly calls the action and is able to get the answer:

LLM not failing at math

Definitions

Let’s establish two definitions:

  • Agent: a LLM that has an objective and a set of actions available to take in order to fulfill its goal;
  • Action: a function that the LLM can call to get its output.
    • Also called “Tool” in other places.

Creating your first agent

We’ll be creating a basic math agent that has access to: sum(), subtract(), multiply() and divide() actions.

To create an agent in RΞASON you’ll need to define its actions and then the agent itself.

Creating the first action

To create an action you need to:

  • Create a new .ts file under src/actions;
  • Export a default function;
  • Add a description for the action and for its parameters for the LLM to understand them.

Let’s create the SUM action we mentioned previously:

src/actions/sum.ts
/**
 * This action will sum two numbers together.
 * You should call this if you need to sum two numbers in order to complete the user's need.
 * @param number1 the first number to sum
 * @param number2 the second number to sum
 */
export default function sum(number1: number, number2: number): number {
  return number1 + number2
}

Two things to notice:

  1. All the is above the sum function in the form of JSDoc;
  2. The function itself is just normal TS/JS code — you can do whatever you wish there as long as you return something in the end.

And that’s it! Pretty simple.

A note on JSDoc

If you never heard of JSDoc before, go here to understand it a bit better.

JSDoc may seem strange & complicated, but what we recommend is the following:

  1. Write the function definition;
  2. Go above it and type /** and your IDE will most-likely auto-complete for you.

JSDoc autocompletion

To verify you’ve correctly wrote JSDoc, you can hover over the function:

Function & parameters description when you hover

Creating the other actions

Awesome! We now have a sum action, however we still need the subtract, multiply and divide actions:

With all four actions created, we now need to create the agent itself.

Agent mental model

Before actually creating your first agent, we need to create the required mental model to understand how they work.

Agents mental model

Three important things to notice:

  • A step is cycle of:
    1. LLM receives a message (be that from your the user or from an action output it previously called);
    2. LLM decides which action to call (and its parameters);
    3. The action is ran;
    4. Your program decides if it want to stop the agent or go for another step.
  • In a step the LLM can:
    • Call an action;
    • Send a normal text message with no action call;
    • Call an action and also send a normal text message.
  • It is up to your program to decide when to stop your agent. Some common stop heuristics are:
    • If the LLM return a message but no action call then probably it wants to end;
    • Create an end() action that the LLM must call when it wants to end;
    • Use a max_step counter — where the LLM can run for at most n number of steps.

Creating the agent

To create an agent in RΞASON:

  • Create a new .ts file under src/agents;
  • Export a const actions = [] containing the available actions for the agent;
  • Add a description for the agent;
  • Call the useAgent() function.

Let’s take a look at the MathAgent:

src/agents/math-agent.ts
import { useAgent } from "tryreason";
import divide from "../actions/divide";
import multiply from "../actions/multiply";
import subtract from "../actions/subtract";
import sum from "../actions/sum";
import answer from "../actions/answer";

// 👇 Define which actions the agent has available
export const actions = [
  sum,
  subtract,
  multiply,
  divide,
  answer,
]

/**
 * You are a helpful math assistant.
 * You only have access to the four basic math operations (addition, subtraction, multiplication, and division).
 * 
 * You *MUST* follow all math rules when answering the user's question.
 * For instance, you cannot divide by zero and you have to follow the order of operations.
 */
export default async function MathAgent(input: string) {
  const agent = await useAgent()
  // 👆 Initialize the agent

  /* 👇 Start running the agent,
        each iteration is a `step` */
  for await (const step of agent.reason(input)) {
    if (step.message && !step.action) {
      agent.stop()
      return step.message.content
    }
    
    if (step.action.name === 'answer') {
      agent.stop()
      return step.action.output
    }
  }
}

Let’s break it down:

  • To define which actions the agent has you need to export const actions = [ ] with the actions;
  • To initialize an agent you call const agent = await useAgent();
  • There are two parts to the prompt of the agent:
    • The agent prompt which is define as a JSDoc above the agent function — and is used as the system intruction;
    • The prompt for the initial message which is passed as the parameter to agent.reason(initialPrompt).
  • To run the agent agent.reason(initialPrompt);
    • agent.reason() is an async generator that you iterate using for await (const step of agent.reason()) {}
  • After each step, you decide if you want to call agent.stop() to stop the agent or not. In this case we’re stopping the agent if:
    • The LLM doesn’t call an action but returns a text message;
    • The LLM calls the answer action.

Cool! Now we need an entrypoint in order to test our agent.

Creating an entrypoint

Let’s create a POST /math entrypoint:

src/entrypoints/math.ts
import MathAgent from "../agents/math-agent"

export default async function* POST(req: Request) {
  const { input } = await req.json()
  
  const answer = await MathAgent(input)
  yield {
    answer
  }
}

Running our agent

If we call the POST /math entrypoint we get:

Response from POST /math


Agent streaming behaviour

A important thing to notice is that there was a lot of data that was streamed even though we only explictly set to stream the final answer in the JSON’s answer property. Why?

  • Because all agents in RΞASON by default stream:
    • What action the LLM choose;
    • Action input/output pairs;
    • The text messages the LLM return.
  • This behaviour can be disabled.

But the cool thing is that if you leave the default streaming agent usage enabled, you can use RΞASON Playground’s chat mode:

RΞASON Playground's chat mode

RΞASON Playground’s chat mode

Since most agents are chat-like, we thought it’d be cool to include a chat interface in the Playground to help test your agents.

For chat mode to work:

  • Your agent needs to use the default streaming behaviour of agents;
  • Whenever a message is sent in the chat, the Playground will make a POST request with a input property in the body. That means your entrypoint needs to read it and pass it to your agent.

Another cool thing that chat mode does is: it handles the memory_id automatically.

But what is memory_id?

About memory_id

Some usecases, like creating a chat-like agent, requires persistance — i.e.: be able to restore previous messages in the chat.

RΞASON offers persistance by default:

  • Every time you call useAgent() a new memory_id is created;
  • When you call useAgent(memory_id) again you can pass a memory_id and that agent will be continue where it left off.

You can disable this.

Under the hood RΞASON uses a local SQLite database to store all the information needed. If you go to your project directory, you’ll see a database.sqlite file.

Let’s test this out:

src/entrypoints/math.ts
import MathAgent from "../agents/math-agent"

export default async function* POST(req: Request) {
  const { input, memory_id } = await req.json()
                // 👆 getting this from the body
                                    // 👇 passing to our agent
  const answer = await MathAgent(input, memory_id)
  yield {
    answer
  }
}

And in our agent:

src/agents/math-agent.ts
import { useAgent } from "tryreason";
// ...

export const actions = [
  // ...
]

/**
 * You are a helpful mat...
 * ...
 */
export default async function MathAgent(input: string, memory_id?: string) {
                                                    // 👆 new parameter
  const agent = await useAgent(memory_id)
                            // 👆 pass the memory_id

  for await (const step of agent.reason(input)) {
      // ...
  }
}

Let’s try it on the Playground:

Using `memory_id`

In the demo above, we didn’t use chat mode just to illutrate how memory_id works under the hood. However, as long as your entrypoint is expecting a memory_id property inside the request’s body, chat mode will automatically send the memory_id.


Agent options

There are some options you can set to change the default behaviour of agents in RΞASON:

src/agents/math-agent.ts
import { useAgent, AgentConfig } from "tryreason";
// ...

export const actions = [
  // ...
]

/**
 * You are a helpful mat...
 * ...
 */
export default async function MathAgent(input: string, memory_id?: string) {
  const agent = await useAgent(memory_id)

  for await (const step of agent.reason(input)) {
      // ...
  }
}

export const config: AgentConfig = {
  // If not set, RΞASON will use the default model set in .reason.config.js
  model: string;

  // The default value is 0.2
  temperature: number;

  /**
   * The maximum number of turns this agent will take before stopping. A turn is defined as a response from the LLM model — calling an action or sending a message.
   */
  max_turns?: number;

  /**
   * If this is `false`, the agent will not automatically stream back to the client the text it receives from the LLM model. This is useful if you want to do something with the text before sending it back to the client.
   * 
   * By default, this is true.
   */
  streamText?: boolean;

  /**
   * If `true`, RΞASON will send the memory ID of the current conversation automatically to the client as the `memory_id` field in the root of the streamed object.
   * This only works if the agent is in stream mode, otherwise the memory ID will not be sent automatically.
   * 
   * The default is `true`.
   */
  sendMemoryID?: boolean; 

  /**
   * If `true`, RΞASON won't automatically save the history of the current conversation. If `true` passing a `memory_id` to `useAgent` will do nothing and `memory_id` will also not be sent.
   * 
   * The default is `false`.
   */
  disableAutoSave?: boolean;
}

Conclusion

Although there is a bit more to learn about agents in RΞASON — such as how to deal with context length of the LLM — this page served as an introduction to them.

Next, we’ll be talking about the final piece of RΞASON puzzle: observability.