Skip to content

Build a CLI chatbot

rig::integrations::cli_chatbot turns an agent into an interactive terminal REPL. It reads input from stdin, keeps the conversation history so each turn has context, streams the agent’s reply to stdout, and stops when you type exit. It is a quick way to try an agent locally without writing your own input loop.

Build an agent and hand it to ChatBotBuilder:

use rig::integrations::cli_chatbot::ChatBotBuilder;
use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let agent = AgentBuilder::new(OpenAI::from_env()?.completion(openai::GPT_5_5))
.preamble("You are a helpful assistant.")
.build();
// Start the REPL. Type "exit" to quit.
ChatBotBuilder::new().agent(agent).build().run().await?;
Ok(())
}

Run it and you get a > prompt. Each answer streams in as it is generated, and the committed messages of every turn (including tool calls and results) are appended to the history passed into the next turn.

When the chatbot wraps an Agent, two builder options are available:

ChatBotBuilder::new()
.agent(agent)
.max_turns(5) // model-call budget per prompt, so the agent can use tools
.show_usage() // print input/output token counts after each answer
.build()
.run()
.await?;

max_turns defaults to 1, which is enough for a plain question-and-answer agent. Raise it when the agent has tools: each tool round trip uses a model call.

Any agent works, including one that pulls context from a vector store on every turn:

use rig::embeddings::EmbeddingsBuilder;
use rig::integrations::cli_chatbot::ChatBotBuilder;
use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};
use rig::vector_store::in_memory_store::InMemoryVectorStore;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpenAI::from_env()?;
let embedding_model = client.embedding(openai::TEXT_EMBEDDING_3_SMALL, None).erase();
let embeddings = EmbeddingsBuilder::new(embedding_model.clone())
.documents(vec![
"Rig is a Rust library for building LLM applications.".to_string(),
"A Rig agent combines a model, a preamble, context and tools.".to_string(),
])?
.build()
.await?;
let index = InMemoryVectorStore::from_documents(embeddings).index(embedding_model);
let agent = AgentBuilder::new(client.completion(openai::GPT_5_5))
.preamble("Answer using the retrieved context.")
.dynamic_context(2, index)
.build();
ChatBotBuilder::new().agent(agent).build().run().await?;
Ok(())
}

See Vector Stores & RAG for loading real documents into the store.

To put something other than an Agent behind the REPL, implement the cli_chatbot::Chat trait and pass the value to .chat(..) instead of .agent(..). The trait has one method: run one turn against the caller-owned history, append the messages it committed, and return the reply text.

use rig::completion::PromptError;
use rig::integrations::cli_chatbot::{Chat, ChatBotBuilder};
use rig::message::Message;
use rig::wasm_compat::WasmCompatSend;
/// Replies with the length of the conversation so far.
struct Echo;
impl Chat for Echo {
async fn chat(
&self,
prompt: impl Into<Message> + WasmCompatSend,
history: &mut Vec<Message>,
) -> Result<String, PromptError> {
let prompt: Message = prompt.into();
history.push(prompt);
let reply = format!("turn {}", history.len());
history.push(Message::assistant(reply.clone()));
Ok(reply)
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
ChatBotBuilder::new().chat(Echo).build().run().await?;
Ok(())
}

A custom Chat prints the whole reply once the turn finishes; only the Agent path streams.