Skip to main content

GoalChain is a simple but effective framework for enabling goal-orientated conversation flows for human-LLM and LLM-LLM interaction.

Project description

GoalChain

GoalChain is a simple but effective framework for enabling goal-orientated conversation flows for human-LLM and LLM-LLM interaction.

Installation

pip install goalchain

Getting started

Let's import the Field, ValidationError, Goal and GoalChain classes, which are the basis for the conversation flow.

from goalchain import Field, ValidationError, Goal, GoalChain 

In this example we will create an AI assistant whose goal is to collect information from a customer about their desired product order. We define the information to be collected using Field objects within the ProductOrderGoal, which is a child of Goal:

  • the product name,
  • the customer's email, and
  • quantity

We also define a validator for the quantity (after type casting to an int). ValidationError is used to pass error messages back to conversation. These messages should be human-readable.

format_hint is a natural language type hint for the LLM's JSON mode output.

def quantity_validator(value):
    try:
        value = int(value)
    except (ValueError, TypeError):
        raise ValidationError("Quantity must be a valid number")
    if value <= 0:
        raise ValidationError("Quantity cannot be less than one")
    if value > 100:
        raise ValidationError("Quantity cannot be greater than 100")
    return value

class ProductOrderGoal(Goal):
    product_name = Field("product to be ordered", format_hint="a string")
    customer_email = Field("customer email", format_hint="a string")
    quantity = Field("quantity of product", format_hint="an integer", validator=quantity_validator)

In case the customer changes their mind, let's create another Goal child class called OrderCancelGoal. We will request an optional reason for the customer's cancellation of the ongoing order. Through specifying that the field is "(optional)" in the description, the LLM will know it isn't necessary to achieve the goal.

class OrderCancelGoal(Goal):
    reason = Field("reason for order cancellation (optional)", format_hint="a string")

Note that the field object names, such as product_name are passed directly to the LLM prompt, and so they are part of the prompt-engineering task, as is every other string.

Essentially the classes we defined are like forms to be filled out by the customer, but they lack instructions. Let's add those by instantiating the classes as objects.

product_order_goal = ProductOrderGoal(
    label="product_order",
    goal="to obtain information on an order to be made",
    opener="I see you are trying to order a product, how can I help you?",
    out_of_scope="Ask the user to contact sales team at sales@acme.com"
)

order_cancel_goal = OrderCancelGoal(
    label="cancel_current_order",
    goal="to obtain the reason for the cancellation",
    opener="I see you are trying to cancel the current order, how can I help you?",
    out_of_scope="Ask the user to contact the support team at support@acme.com",
    confirm=False
)

We define

  • an internal label to be used (also part of our prompt-engineering task),
  • the goal, expressed as a "to ..." statement,
  • a default opener - something the AI assistant will use given no prior input,
  • and importantly, instructions for the AI assistant as to what they should do in case of an out of scope user query

The confirm flag determines whether the AI assistant will ask for confirmation once it has all of the required information defined using the Field objects. It is True by default. We don't need a confirmation for the order cancellation goal, since it is in itself already a kind of confirmation.

Next we need to connect the goals together.

product_order_goal.connect(goal=order_cancel_goal, 
                           user_goal="to cancel the current order", 
                           hand_over=True, 
                           keep_messages=True)

The user_goal is another "to ..." statement. Without hand_over=True the AI agent would reply with the canned opener. Setting it to True ensures the conversation flows smoothly. Sometimes you may want a canned response, other times not.

keep_messages=True means the order_cancel_goal will receive the full history of the conversation with product_order_goal, otherwise it will be wiped. Again, sometimes a wipe of the conversation history may be desired, such as when simulating different AI personalities.

Let's also consider the possibility of a really undecisive customer. We should also give them the option to "cancel the cancellation".

order_cancel_goal.connect(goal=product_order_goal, 
                          user_goal="to continue with the order anyway", 
                          hand_over=True, 
                          keep_messages=True)

At some point you may have wondered if you can make a goal without any Field objects. You can! Such a goal is a routing goal defined only be the connections it has. This is useful for example in a voice-mail menu system.

You may also be curious whether you can connect a goal to itself. You can! This is useful for example when using confirm=False with the Goal-inheriting object, where you require sequential user input of some variety.

You can also chain connects, e.g. goal.connect(...).connect(...).connect(...)

Finally, let's use GoalChain to set the initial goal and test our AI sales assistant!

goal_chain = GoalChain(product_order_goal)

Note that each goal can use a separate LLM API as enabled by LiteLLM, and if you have the required environment variables set, you can use any model from the supported model providers.

The default model is "gpt-4-1106-preview", that is:

product_order_goal = ProductOrderGoal(...
    model="gpt-4-1106-preview", 
    json_model="gpt-4-1106-preview"
)

You can also pass LiteLLM common parameters using params, for example:

product_order_goal = ProductOrderGoal(...
    model="gpt-4-1106-preview", 
    json_model="gpt-4-1106-preview",
    params={"temperature": 1.5, "max_tokens": 10}
)

You can also use params to call local models using VLLM.

When using the default "gpt-4-1106-preview" model, remember to set the OPENAI_API_KEY environment variable.

import os
os.environ["OPENAI_API_KEY"] = "sk-ABC..."

Note: The code so far is available as a gist. Paste it into a Jupyter notebook, preceded by !pip install goalchain to get started with the live example below.

Usually it is the user who prompts the AI agent first, but if this is not the case, we call get_response without any arguments, or use None as the argument:

goal_chain.get_response()
{'type': 'message',
 'content': 'Great choice! Could you please provide me with your email address to proceed with the order?',
 'goal': <__main__.ProductOrderGoal at 0x7f8c8b687110>}

GoalChain returns a dict containing the type of response (either message or data), the content of the response (right now just our canned response) and the current Goal-inheriting object.

Let's query our AI assistant with a potential purchase.

goal_chain.get_response("Hi, I'd like to buy a vacuum cleaner")
{'type': 'message',
 'content': 'Great! Could you please provide your email address so we can send the confirmation of your order?',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

The AI assistant is working towards achieving its current goal, and gathering the required information for an order.

goal_chain.get_response("Sure, it is john@smith.com")
{'type': 'message',
 'content': 'Thank you, John. Which model of vacuum cleaner would you like to order?',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}
goal_chain.get_response("The 2000XL model")
{'type': 'message',
 'content': 'How many of the 2000XL model would you like to order?',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}
 

Let's test whether our AI assistant can handle a cancellation of the current order.

goal_chain.get_response("Actually I changed my mind, cancel this order")
{'type': 'message',
 'content': 'Of course, I can assist with that. Could you please tell me the reason for the cancellation?',
 'goal': <__main__.OrderCancelGoal at 0x7ff0fb275650>}
 

It worked. Note that the returned goal is now of type OrderCancelGoal. We've switched goals. Let's also test whether we can switch back.

goal_chain.get_response("Actually, yeah, I would like to buy the vacuum cleaner")
{'type': 'message',
 'content': 'Understood. How many of the 2000XL model would you like to order?',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

We're back to the ProductOrderGoal.

goal_chain.get_response("1 please")
{'type': 'message',
 'content': 'To confirm, you would like to order one 2000XL vacuum cleaner and the order will be sent to john@smith.com, is that correct?',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

The AI assistant confirms our order. If we didn't like this behaviour we would use confirm=False.

Let's see how the assistant responds to an out of scope query.

goal_chain.get_response("Is it a good vacuum cleaner? What do you think?")
{'type': 'message',
 'content': "For product reviews and additional information, I recommend contacting our sales team at sales@acme.com. They can help with your inquiries. Meanwhile, can you please confirm if you'd like to proceed with the order for one 2000XL vacuum cleaner to john@smith.com?",
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

The AI assistant redirects us to the sales team inbox as defined earlier, and re-iterates the confirmation.

But let's throw a curve-ball...

goal_chain.get_response("Ok, I'd actually like to make that an order of 500")
{'type': 'message',
 'content': "Just to clarify, you'd like to order 500 units of the 2000XL vacuum cleaner, with the order confirmation sent to john@smith.com. Is that correct?",
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}
goal_chain.get_response("Yes")
{'type': 'message',
 'content': 'I’m sorry, but I need to inform you that the quantity cannot be greater than 100 for an order. If you would like to proceed with an order within this limit, please let me know.',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

The validator we use has given enough information to the AI assistant to justify why it cannot process this quantity via the ValidationError message.

Note that GoalChain only validates inputs once the Goal has been completed for token efficiency and performance reasons. If you'd like to validate inputs as you go, you have two options:

  1. Use a Goal with only one Field, and confirm=False. Chain these goals instead of using multiple fields in a single Goal.

  2. Use a soft-prompt, e.g. quantity = Field("quantity of product (no more than 100)", format_hint="an integer"). This approach is not foolproof, so it is still recommended to use a validator. The user will receive immediate feedback, however.

Let's complete the order.

goal_chain.get_response("Alright, I'll guess I'll just go with 1")
{'type': 'message',
 'content': 'To confirm, you would like to order one 2000XL vacuum cleaner and the order will be sent to john@smith.com, is that correct?',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}
goal_chain.get_response("That's right")
{'type': 'data',
 'content': {'customer_email': 'john@smith.com',
  'product_name': '2000XL',
  'quantity': 1},
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

The content returned is a dictionary parsed from the output of the LLM's JSON mode. The keys are our field instance names. We can now use the data to perform some kind of action, such as processing the order of our hypothetical 2000XL vacuum cleaner.

Note that in reality, if you were building such a system, you would need to make a dedicated product-lookup goal as not to allow arbitrary or meaningless product names.

Let's send our confirmation the order has been processed via simulate_response. We will also use rephrase = True to rephrase the output, which will appear more natural in case the customer frequently interacts with the goal.

goal_chain.simulate_response(f"Thank you for ordering from Acme. Your order will be dispatched in the next 1-3 business days.", rephrase = True)
{'type': 'message',
 'content': 'We appreciate your purchase with Acme! Rest assured, your order will be on its way within the next 1 to 3 business days.',
 'goal': <__main__.ProductOrderGoal at 0x7ff0fb283090>}

At this point we may end the session or connect back to a menu or routing goal for further input.

If you would like to customise or contribute to GoalChain, or report any issues, visit the GitHub page.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

goalchain-0.0.3.tar.gz (10.4 kB view details)

Uploaded Source

Built Distribution

goalchain-0.0.3-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

File details

Details for the file goalchain-0.0.3.tar.gz.

File metadata

  • Download URL: goalchain-0.0.3.tar.gz
  • Upload date:
  • Size: 10.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.0 CPython/3.11.7

File hashes

Hashes for goalchain-0.0.3.tar.gz
Algorithm Hash digest
SHA256 f95ac8e00f11da47610619df7362bcf856290ed2cafa5a1c43e891a234adfc44
MD5 44f67f65a6d89eee7ecd3d3e5e59bc9a
BLAKE2b-256 72d3a0af8a7503a95f147787b2d410c1dd7ca1b679e8c1944d41878f330cffeb

See more details on using hashes here.

File details

Details for the file goalchain-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: goalchain-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 9.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.0 CPython/3.11.7

File hashes

Hashes for goalchain-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 88b52ead84fc54936c1e248f28b093a68d47f92c8ac48ef8b41f85554436331f
MD5 c231607813da36007f5571852c201a85
BLAKE2b-256 0778d805826e56fae33a0af2a3a6ff8c58bea078072e61e76f5ef08feb5f3a8b

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page