Executing Actions

Tutorial 1: Simple Actions

At the heart of ralf is the Action class. An Action object represents a basic data processing block and can take one of two forms: (1) a call to a Large Language Model (LLM) or (2) a Python function.

The first example we will look at is defining and executing simple LLM-based action. We begin by importing the Action and defining our action. When defining an action, we can provide a prompt that we expect that LLM to complete for us. In this example we are looking for the LLM to tell us what the capital of Australia is. After defining the action, we can use the object’s __call__ method to execute it.

[1]:
from ralf.dispatcher import Action

get_aussie_capital = Action(prompt="The capital of Australia is")
get_aussie_capital()
[1]:
{'output': 'Canberra.'}

Note that in the above example, we did not specify anything about the LLM itself. In this case, ralf used the default LLM configuration, which is specified in ralf.utils. However, we can also directly specify one or more model configuration parameters when creating the action:

[2]:
get_aussie_capital = Action(
    prompt="The capital of Australia is",
    model_config={
        'model': 'gpt-3.5-turbo',
        'temperature': 0.0,
        'stop': ['.']
    }
)
get_aussie_capital()
[2]:
{'output': 'Canberra'}

Tutorial 2: Actions with Context

So far, we have seen how to create an LLM-based action with a full prompt, in which case we simply want to obtain a completion of a fully-formed piece of text. In many situations, we do not have a full prompt, but instead have a prompt template. A prompt template contains placeholders that need to be filled in with appropriate context before being submitted to the LLM for completion

For example, we may want to define an action that can ask an LLM for the capital of any country. Then, when we go to execute the action, we can provide the name of the country whose capital we wish to know. We can do this by providing a prompt template when defining the action, then providing a context dictionary with entries corresponding to the placeholders when we execute the action.

[3]:
get_country_capital = Action(prompt_template="The capital of {country} is")
get_country_capital(context={'country': 'Egypt'})
[3]:
{'output': 'Cairo.'}

Tutorial 3: Python-Based Actions

Remember that ralf actions can either involve calls to LLMs or execution of Python functions. In this example, we will explore the latter. To define a Python-based action, you can simply pass in a Python function when creating the action object:

[4]:
import re

def sum_from_text(text: str) -> int:
    """Finds all numbers in the text and sums them"""

    text_counts = re.findall('[0-9]+', text)
    return sum([int(x) for x in text_counts])

count_adder = Action(func=sum_from_text,
                     input_name='string_with_numbers',
                     output_name='summed_numbers')

count_adder({"string_with_numbers" : "12, 5, 10 and 6"})
[4]:
{'summed_numbers': 33}

While defining an action to simply execute a Python function might seem trivial at first, we will see how this is useful when creating a sequence of actions in the next example.

Tutorial 3: Action Sequences

In many applications, we want to execute a sequence of steps that processes some information and arrives at a final result. Some steps in this process might be very well defined (e.g., arithmetic) or might involve some interaction with other external resources (e.g., querying a database), in which case you might write Python functions to implement them. In other cases, you might wish to use a call to an LLM to exectue the task. ralf makes it easy to chain together actions of both types, as we will see in the next example.

Say we have a piece of text that represents a customer’s order. We’d like to determine how many fruits are in the customer’s order and generate a natural language response to them based on the number of fruits. We can begin by creating the actions invovled in the process. Here, we will have three actions. The first will use an LLM’s knowledge of fruits, and its language understanding capabilities, to interpret a user’s order and enumerate the number of fruits of each type.

[5]:
enumerate_fruits = Action(
                    prompt_template="I'm going to give you sentence. "
                    "Please enumerate how many fruits of each type are mentioned. Ignore non-fruits. "
                    "Format should be fruit_name:<fruit_count> with commas in between. "
                    "Sentence: {utterance}",
                    output_name='fruit_counts'
)

The next action will take the output of the previous fruit enumeration action and sum the counts together to arrive at a total number of fruits. The reason we may want to do this is to avoid relying on the LLMs arthmetic capabilities, which have been shown to be unreliable (though progress is being made on this front). Since we know how to do basic arthmetic in a reliable manner, it is more appropriate to use a Python function for this step. We can create this action using the sum_from_text function we defined previously.

[6]:
sum_fruits = Action(func=sum_from_text,
                    input_name='fruit_counts',
                    output_name='fruit_total'
)

Finally, we create a third action that will draft a response to the customer based on the fruit total.

[7]:
create_reply = Action(
    prompt_template="A customer is trying to purchase {fruit_total} fruits from our company Fruits'R'Us. "
                    "Write a reply to them restating their order and explaining the policy if they exceed "
                    "the maximum of 10 fruits per order. Otherwise politely thank them for their business.",
    output_name='reply'
)

Now, we will see how we can execute all 3 actions in a sequence, with appropriate passing of the outputs of one action into the inputs of the next. To do this, we will leverage the Dispatcher class within ralf. The job of the dispatcher object is to handle the details of how exactly to execute an action, or a sequence of actions.

In this example, we will find out how to execute an action sequence. We begin by defining a dispatcher object. Then, we simply string together the three actions we just defined by wrapping them into a standard Python list. Now, we call the execute method in the dispatcher object– passing it the list of actions and the customer’s order.

[9]:
from ralf.dispatcher import ActionDispatcher
ad = ActionDispatcher()

script = [enumerate_fruits, sum_fruits, create_reply]

input_text = "I'd like to order 4 bananas, 6 oranges, a cabbage and 2 honeycrips"
output, _ = ad.execute(script, utterance=input_text)

print(output['reply'])
Dear valued customer,

Thank you for choosing Fruits'R'Us for your fruit needs. We appreciate your business.

I understand that you would like to purchase 12 fruits from us. To confirm, you are requesting 12 fruits in total, is that correct?

Please note that our policy allows a maximum of 10 fruits per order. However, we would be happy to assist you in placing multiple orders if you require more than 10 fruits.

Thank you for your understanding and please let us know how we can assist you further.

Best regards,

[Your name]

Tutorial 4: Using YAML Files

So far, the LLM prompts we have seen have been relatively simple and short. In many applications, however, prompts (or prompt templates) can contain large amounts of text. In such cases, it is inconvenient to store the prompts (or prompt templates) inside the source code, and it is recommended that one use YAML files to store these instead.

With ralf, we can easily perform YAML-based prompt template specification when defining an object from the Dispatcher class. The Dispatcher constructor takes as input the path to what we call a RALF data directory (typically named ralf_data). Inside of this directory is where we place the YAML file that contains the prompt templates we want our dispatcher to know about (the file should be named prompts.yml). An example prompts.yml file can be found in the demos directory.

In addition to prompts.yml, the RALF data directory should also contain a second YAML file for specifying model configurations. For each prompt template in prompts.yml with an associated model name specified, ralf will excite LLM-based actions using the model configuration of this name in models.yml.

We can see in the example code below that using YAML-based prompt template and model specification can greatly simplify our Python code!

[13]:
from ralf.dispatcher import ActionDispatcher

ad = ActionDispatcher(dir='../../demos/ralf_data')

enumerate_fruits = Action(prompt_name='enumerate_fruits')
output, _ = ad.execute([enumerate_fruits], utterance='i have 2 strawberry shortcakes, 4 bananas and 6 hot dogs')

print(output)
{'output': " 'strawberries':2, 'bananas':4"}