Tools
Give the model functions it can call, and let chat-rs run the loop.
A tool is a function the model can ask to run. You declare tools, hand them to the builder, and chat-rs runs the loop: the model asks for a tool, chat-rs runs it, feeds the result back, and lets the model keep going until it has an answer.
Declare a tool
Annotate an async function with #[tool] from the tools-rs crate. The doc
comment becomes the tool's description, and the parameters become its schema.
use tools_rs::{collect_tools, tool};
#[tool]
/// Look up the current weather for a city.
async fn get_weather(city: String) -> String {
format!("It's 22C and sunny in {city}.")
}Hand them to the chat
collect_tools() gathers every #[tool] in scope. with_tools registers them
with an always-run strategy, and with_max_steps bounds how many times the loop
may go around.
use chat_rs::{ChatBuilder, ChatOutcome, ollama::OllamaBuilder, parts, types::messages};
use tools_rs::collect_tools;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = OllamaBuilder::new().with_model("llama3.2").build();
let mut chat = ChatBuilder::new()
.with_tools(collect_tools())
.with_model(client)
.with_max_steps(5)
.build();
let mut messages = messages::from_user(parts!["What's the weather in Lisbon?"]);
if let ChatOutcome::Complete(res) = chat.complete(&mut messages).await.map_err(|e| e.err)? {
if let Some(text) = res.content.parts.text_response() {
println!("{text}");
}
}
Ok(())
}with_max_steps(n) caps the agentic loop at n iterations. It defaults to 1,
which means no tool loop, so set it when you expect the model to call tools.
Going further
- Tools in Python: write tools as Python functions and load them at runtime.
- Human in the loop: pause for a person to approve, reject, or edit a call before it runs.
- Native tools: provider-built tools like code execution and search, running alongside your own.
- Tools with context: attach metadata to tools and decide per call what should happen.