Today, most AI agents are built by predefining a set of actions (or "tools") they can take: refund a Stripe charge, create an entry in a CRM, send a Slack message.
In fact, many companies are building LLM-specific tooling around their APIs to better connect into LLMs, such as wrappers for Claude and GPT's Tool Use / Function Calling[0].
But we also know that LLMs are pretty good at understanding programming interfaces and writing code on their own.
What if we gave an agent the ability to create its own tools by writing code and executing them safely on Riza using our new Tools API, and avoiding building any LLM-specific tooling at all?
We built a simple demo agent (using Anthropic's Claude 3.5 Sonnet model) that does exactly that, and we were surprised at how well it was able to bootstrap its own skills by writing code on the fly in response to user requests.
We'll walk you through a demo interaction below, but we encourage you to try it out for yourself!
[0] e.g., Stripe
Demo
For demo purposes, we're going to operate on the Stripe API, because it's well known, easy to configure, and offers a safe testmode environment. However, we expect these results to be reproducible on other well-known API services.
On a fresh session, we provide the LLM with just three tools: two to request user input
(request_user_input
[0], show_options
[1]), and one to create a new tool
(create_tool
[2]).
This demo is a real session I had using our demo agent, formatted for clarity. Take a look at the rightmost column to observe how the agent is growing its own skillset on the fly.
[0] request_user_input
definition
and
implementation
[1] show_options
definition
and
implementation
[2] create_tool
definition
and
implementation
Chat messages | SelfLearningAgent client | Agent's toolset |
---|---|---|
System promptUser: You are an autonomous agent that can create tools to perform tasks. You can write Typescript code to create tools. You can also use the tools you have created to perform tasks.
You should use the
Each tool should be written in Typescript code, as a function named function execute(input: unknown) {
You must also write the
The If you write a tool that uses an external API, you must make plain HTTP requests to the API, using only the node fetch library. You can assume that any authentication is handled outside of the tool, and does not need to be handled in the tool.
When executing tools, you should use the Commentary: Note that the system prompt does not contain references to Stripe or any other service; it is generic, so the agent can be shaped into whatever you prompt it to be. |
|
|
User: list all stripe customers with the email david@riza.io | ||
Assistant: To accomplish this task, we need to create a tool that can interact with the Stripe API to list customers with a specific email. Then, we'll use that tool to fetch the customers with the email "david@riza.io". Let's start by creating the tool. | ||
Assistant: [Tool Use] View tool use argscreate_tool({
Commentary: The
|
||
Handle use of create_tool by calling Riza's
Create Tool API
|
||
User: [Tool Use Result] Created tool View LLM-generated Tool source codeasync function execute(input: { email: string }): Promise |
Commentary: The tool that the LLM just created is now available for it to use. |
|
Assistant: Great! We have successfully created a tool called
list_stripe_customers_by_email that can fetch Stripe customers filtered by email address. Now,
let's use this tool to list all Stripe customers with the email "david@riza.io".
|
||
Assistant: [Tool Use] list_stripe_customers_by_email({ |
||
Handle use of list_stripe_customers_by_email by calling Riza's
Execute Tool API on the previously created
tool, and return the output of the Riza execution
|
||
User: [Tool Use Result] View full response[ |
||
Assistant: I've successfully retrieved the Stripe customers associated with the email address "david@riza.io". Here's a summary of the results:
Both customers have the same email address (david@riza.io) as requested. They appear to be test customers (livemode: false) and have no balance or default payment source set. Is there any specific information about these customers you'd like to know more about, or would you like to perform any other actions with this data? |
||
Assistant: [Tool Use] |
||
Handle use of request_user_input by prompting for user input in the CLI, and return the response
|
||
User: [Tool Use Result] change the email of the first one to david+demo@riza.io |
||
Assistant: To change the email of the first customer, we need to create a new tool that can update a Stripe customer's email. Then, we'll use this tool to change the email of the customer with ID "cus_RFmj2hHH2mxihm". Let's start by creating the update tool. | ||
Assistant: [Tool Use] View tool use argscreate_tool({ |
||
Handle use of create_tool by calling Riza's
Create Tool API
|
||
User: [Tool Use Result] Created tool View LLM-generated Tool source codeasync function execute(input: { customerId: string; newEmail: string }): Promise |
|
|
Assistant: Great! We have successfully created a tool called
update_stripe_customer_email that can update a Stripe customer's email address. Now, let's use
this tool to change the email of the first customer (ID: cus_RFmj2hHH2mxihm) to "david+demo@riza.io".
|
||
Assistant: [Tool Use] update_stripe_customer_email({ |
||
Handle use of update_stripe_customer_email by calling Riza's
Execute Tool API on the previously created
tool, and return the output of the Riza execution
|
||
User: [Tool Use Result] View full response{ |
||
Assistant: The email address for the customer has been successfully updated. Here's a summary of the changes:
The update was successful, and the customer's email address has been changed to "david+demo@riza.io" as requested. All other customer details remain the same. Is there anything else you would like me to do with this customer's information or any other Stripe-related tasks? |
||
Assistant: [Tool Use] |
||
Handle use of request_user_input by prompting for user input in the CLI, and return the response
|
||
User: [Tool Use Result] add a new card to this customer, using a card token |
||
Assistant: I apologize, but I realize we don't have a specific tool to add a new card to a customer using a card token. We'll need to create a new tool for this purpose. Let's create a tool that can add a new card to a Stripe customer using a card token. | ||
Assistant: [Tool Use] View tool use argscreate_tool({ |
||
Handle use of create_tool by calling Riza's
Create Tool API
|
||
User: [Tool Use Result] Created tool View LLM-generated Tool source codeasync function execute(input: { customerId: string; cardToken: string }): Promise |
|
|
Assistant: Excellent! We've successfully created a new tool called
Now, to add a new card to the customer (ID: cus_RFmj2hHH2mxihm), we need a card token. Card tokens are typically generated on the client-side using Stripe.js or a mobile SDK, and they are single-use tokens. As we don't have a real card token available, we'll need to ask the user to provide one. Let's request the card token from the user: |
||
Assistant: [Tool Use] |
||
Handle use of request_user_input by prompting for user input in the CLI, and return the response
|
||
User: [Tool Use Result] tok_mastercard |
||
Assistant: Thank you for providing the card token. Now, let's use our new tool to add this card to the customer's account. | ||
Assistant: [Tool Use] add_card_to_stripe_customer({ |
||
Handle use of add_card_to_stripe_customer by calling Riza's
Execute Tool API on the previously created
tool, and return the output of the Riza execution
|
||
User: [Tool Use Result] View full response{ |
||
Assistant: Great news! The new card has been successfully added to the customer's account. Here's a summary of the added card:
The card has been successfully added to the customer's account and can now be used for future transactions. The CVC check passed, which is a good sign, although it's worth noting that for a real card, address verification (address_line1_check and address_zip_check) might also be important. Is there anything else you'd like me to do with this customer's account or any other Stripe-related tasks? |
||
Assistant: [Tool Use] |
||
Handle use of request_user_input by prompting for user input in the CLI, and return the response
|
||
User: [Tool Use Result] charge this customer $20 using the new card |
||
Assistant: I understand you want to charge this customer $20 using the new card we just added. However, we don't have a pre-existing tool to charge a customer. Let's create a new tool for this purpose. | ||
Assistant: [Tool Use] View tool use argscreate_tool({ |
||
Handle use of create_tool by calling Riza's
Create Tool API
|
||
User: [Tool Use Result] Created tool View LLM-generated Tool source codeasync function execute(input: { customerId: string; amount: number; currency: string; cardId: string }): Promise |
|
|
Assistant: Great! We've successfully created a new tool called
Now, let's use this tool to charge the customer $20 using the new card we just added. We'll need to convert $20 to cents (2000 cents) for the Stripe API. We'll use the customer ID, card ID, and amount in our request. |
||
Assistant: [Tool Use] charge_stripe_customer({ |
||
Handle use of charge_stripe_customer by calling Riza's
Execute Tool API on the previously created
tool, and return the output of the Riza execution
|
||
User: [Tool Use Result] View full response{ |
||
Assistant: Excellent! The charge has been successfully processed. Here's a summary of the transaction:
The charge of $20 has been successfully processed and captured from the customer's new card. The transaction was approved by the network and completed without any issues. The customer can access their receipt using the provided receipt URL. Is there anything else you would like me to do regarding this customer or any other Stripe-related tasks? |
I'll terminate the demo here because I think you get the point. But we've basically just created a basic Stripe agent that bootstrapped itself on the fly.
Take a look at the right column. During this session, the agent wrote four of its own skills, and used them. You could easily take a "snapshot" of your agent in a useful session and use it again later, making its tools deterministic.
Theoretically, you could choose to have the agent not save any of its tools and always write its executions from scratch. In fact, that is effectively what this demo shows, since we started from zero.
However, an interesting extension would be to give the agent a modify_tool
tool to iterate (improve) on
its skills, much like the "Skill Library" in the Voyager agent that also
learned autonomously.
We won't get into that in this blog post, but stay tuned for more! And come chat with us in our Discord if you found this interesting!