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.
Minimal example
Section titled “Minimal example”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.
Options
Section titled “Options”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.
A RAG chatbot
Section titled “A RAG chatbot”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.
Bring your own chat type
Section titled “Bring your own chat type”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.
See also
Section titled “See also”- Agents: how agents are built and prompted.
- Streaming: the stream the chatbot consumes.
- Build a RAG system: a fuller agent worth wrapping in a REPL.
