Hello world! In this guide, we're going to talk about how you can get started with using AI agents to create a content writer that will use the Serper.dev API to search Google for results on your query, then use the results together with GPT-4o to create a summary of the results and finally create an article about it.
Interested in just deploying or got stuck during the tutorial? Have a look at the repository.
Setting up
To get started, we'll create a new project using cargo-shuttle init
, making sure to pick Axum as the framework. After that, we'll install the dependencies we need:
You will also additionally need API keys from Serper and OpenAI, which you will put in a new Secrets.toml
file in your project root:
Error handling
Before we get started, let's quickly define some error types. We can add this to an errors.rs
file and use thiserror
. The reason why we'll do this is for error propagation: instead of manually pattern matching, if we want the error to be propagated we can use this enum as the error return type then use error propagation:
This is enabled by ApiError
implementing From<T>
, allowing easy propagation.
Building AI Agents
Our first step will be defining a generic interface that all of our autonomous agents will work with. We'll be creating two agents:
- A researcher (takes some data from a Google search, feeds it into ChatGPT and asks it to summarize the information)
- A writer (takes the summary and writes an article about it)
It will look something like this:
Why do we need the other 3 methods if prompt()
already uses self
? This is because as part of the Agent
trait, the prompt function cannot reference types that it doesn't know about. If we have a struct that has the async_openai::Client
type already, we need to create a method from the Agent
trait to be able to access the client.
If you're only creating one agent, typically you don't need a specific trait. However, if you wanted to create more agents in the same library or application, it would be a good idea to have a generic interface to hold all of the relevant methods!
Let's define our Researcher
struct, which will hold the data and methods for us to query the Serper API and then use the data to prompt a model:
Next, we can implement the Agent
trait for our struct:
Note that here, the name()
and client()
functions are mostly boilerplate. If you wanted to extend this even further, you could use a macro to get rid of this totally.
The system message will be for pre-prompting the model. When we prompt the model, the system message will be passed in and then the bot will respond to the prompt according to the system message. In models where system messages don't exist, this would simply represent the text before you put your prompt.
We also implement two methods for impl Researcher
: one to initialise the struct itself, and then one for preparing the data to send into our agent pipeline:
The Writer
half of our AI agents will mostly be the same, save the reqwest::Client
. We need to implement Agent
alongside it, however,
Finally, we'll go back and fill the prompt
method back in on the default Agent
trait so that we have a default method implementation (and therefore don't need to keep re-implementing it):
Writing our web service
Now that the hard work is over - we can implement the AI agents in our web application!
To get started, we'll create an AppState
struct that implements Clone
. Typically, this is a trait bound set by Axum or pretty much any Rust-based framework that you use. In it we'll have our Researcher
and Writer
struct:
Next, we will write our handler endpoint that will take in a JSON input, run the agent pipeline and then return the end result:
And that's basically it!
We can then hook it all up by adding our endpoint to the router:
Deployment
Now all we need to do is use shuttle deploy
(adding the --ad
flag if on an uncommitted Git branch) and watch the magic happen!
Extending this project
Making a Pipeline struct for your agents
So let's say you've built this example, and want to go even further. What about pulling in another agent that generates a Twitter post or a LinkedIn post. At this point, you probably want to build a pipeline that holds all your agents, then you can just write .run_pipeline()
and it'll do everything for you.
To do this, you could create a Pipeline
trait does two things:
- Initialise the agents set (and return it as a
Vec<Box<dyn Agent>>
) - Run the vector as a pipeline, where the results of the previous agent gets fed into the next one
However, you may run into an issue with your Agents needing to implement Sized
. This is because Clone
requires an object to be a known size at compile-time - otherwise it won't work! To fix this, we can wrap the dyn
type in a Box
, allocating it on the heap. This works because the Clone
trait requires the type to have a known, static size at compile-time.
Additionally, you might also receive an error with not being able to compile because of the prompt()
method being async. You can fix this by adding the async_trait
crate, then using the attribute macro above your code:
Note that for every impl Agent for T
, you'll also need to remember to add the async trait macro. Otherwise, you'll get an error about the lifetime annotations not matching!
Updating the prompt
If you're not happy with the prompt results, don't forgetyou can always update the message prompt that gets sent to your model!
Finishing Up
By leveraging Rust with the power of GPT-4o, you can develop a robust AI-powered content writer.
Additional Resources
For further learning and details, refer to: