use crate::types::{
CacheControl, ContentBlock, CustomTool, Message, MessagesRequest, Role, SystemBlock,
SystemPrompt, ToolDefinition, ToolResultContent,
};
#[derive(Debug, Clone)]
pub struct ConversationBuilder {
messages: Vec<Message>,
tools: Vec<ToolDefinition>,
system: Option<SystemPrompt>,
}
impl ConversationBuilder {
pub fn new() -> Self {
Self {
messages: Vec::new(),
tools: Vec::new(),
system: None,
}
}
pub fn with_system(mut self, prompt: impl Into<String>) -> Self {
self.system = Some(SystemPrompt::String(prompt.into()));
self
}
pub fn with_cached_system(mut self, prompt: impl Into<String>) -> Self {
self.system = Some(SystemPrompt::Blocks(vec![SystemBlock {
block_type: "text".into(),
text: prompt.into(),
cache_control: Some(CacheControl::ephemeral()),
}]));
self
}
pub fn with_tool(mut self, tool: impl Into<ToolDefinition>) -> Self {
self.tools.push(tool.into());
self
}
pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools.extend(tools);
self
}
pub fn with_cached_tool(mut self, mut tool: CustomTool) -> Self {
tool.cache_control = Some(CacheControl::ephemeral());
self.tools.push(ToolDefinition::Custom(tool));
self
}
pub fn add_user_message(&mut self, content: impl Into<String>) -> &mut Self {
self.messages.push(Message::user(content));
self
}
pub fn add_assistant_message(&mut self, content: impl Into<String>) -> &mut Self {
self.messages.push(Message::assistant(content));
self
}
pub fn add_assistant_with_blocks(&mut self, content: Vec<ContentBlock>) -> &mut Self {
self.messages.push(Message {
role: Role::Assistant,
content,
});
self
}
pub fn add_tool_result(
&mut self,
tool_use_id: impl Into<String>,
result: impl Into<String>,
) -> &mut Self {
self.messages
.push(Message::tool_result(tool_use_id, result));
self
}
pub fn add_tool_error(
&mut self,
tool_use_id: impl Into<String>,
error_message: impl Into<String>,
) -> &mut Self {
self.messages.push(Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: tool_use_id.into(),
content: Some(ToolResultContent::Text(error_message.into())),
is_error: Some(true),
}],
});
self
}
pub fn messages(&self) -> &[Message] {
&self.messages
}
pub fn tools(&self) -> &[ToolDefinition] {
&self.tools
}
pub fn system(&self) -> Option<&SystemPrompt> {
self.system.as_ref()
}
pub fn clear_messages(&mut self) -> &mut Self {
self.messages.clear();
self
}
pub fn build(&self, model: impl Into<String>, max_tokens: u32) -> MessagesRequest {
let mut request = MessagesRequest::new(model, max_tokens, self.messages.clone());
if let Some(system) = &self.system {
request.system = Some(system.clone());
}
if !self.tools.is_empty() {
request.tools = Some(self.tools.clone());
}
request
}
pub fn into_request(self, model: impl Into<String>, max_tokens: u32) -> MessagesRequest {
self.build(model, max_tokens)
}
pub fn estimate_tokens(&self) -> usize {
let counter = crate::tokens::TokenCounter::new();
let mut total = 0;
if let Some(system) = &self.system {
total += counter.count_system_prompt(system);
}
for message in &self.messages {
total += counter.count_message(message);
}
for tool in &self.tools {
total += counter.count_tool(tool);
}
total
}
pub fn fits_in_context(
&self,
model: &crate::models::Model,
max_tokens: u32,
use_extended_context: bool,
) -> bool {
let counter = crate::tokens::TokenCounter::new();
let request = self.build(model.anthropic_id, max_tokens);
counter
.validate_context_window(&request, model, use_extended_context)
.is_ok()
}
}
impl Default for ConversationBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_basic_conversation() {
let mut conv = ConversationBuilder::new();
conv.add_user_message("Hello");
conv.add_assistant_message("Hi there!");
conv.add_user_message("How are you?");
assert_eq!(conv.messages().len(), 3);
assert_eq!(conv.messages()[0].role, Role::User);
assert_eq!(conv.messages()[1].role, Role::Assistant);
}
#[test]
fn test_with_system() {
let conv = ConversationBuilder::new().with_system("You are helpful");
assert!(conv.system().is_some());
match conv.system().unwrap() {
SystemPrompt::String(s) => assert_eq!(s, "You are helpful"),
_ => panic!("Expected String variant"),
}
}
#[test]
fn test_with_tools() {
let tool = CustomTool::new("test_tool", "A test tool", json!({"type": "object"}));
let conv = ConversationBuilder::new().with_tool(tool);
assert_eq!(conv.tools().len(), 1);
match &conv.tools()[0] {
ToolDefinition::Custom(t) => assert_eq!(t.name, "test_tool"),
_ => panic!("Expected Custom tool"),
}
}
#[test]
fn test_tool_result() {
let mut conv = ConversationBuilder::new();
conv.add_tool_result("toolu_123", "success");
assert_eq!(conv.messages().len(), 1);
assert_eq!(conv.messages()[0].role, Role::User);
match &conv.messages()[0].content[0] {
ContentBlock::ToolResult { tool_use_id, .. } => {
assert_eq!(tool_use_id, "toolu_123");
}
_ => panic!("Expected ToolResult"),
}
}
#[test]
fn test_build_request() {
let mut conv = ConversationBuilder::new().with_system("Test system");
conv.add_user_message("Test message");
let request = conv.build("claude-sonnet-4-5-20250929", 1024);
assert_eq!(request.model, "claude-sonnet-4-5-20250929");
assert_eq!(request.max_tokens, 1024);
assert_eq!(request.messages.len(), 1);
assert!(request.system.is_some());
}
#[test]
fn test_clear_messages() {
let mut conv = ConversationBuilder::new().with_system("System");
conv.add_user_message("Message 1");
conv.add_user_message("Message 2");
assert_eq!(conv.messages().len(), 2);
conv.clear_messages();
assert_eq!(conv.messages().len(), 0);
assert!(conv.system().is_some()); }
#[test]
fn test_cached_system() {
let conv = ConversationBuilder::new().with_cached_system("Cached prompt");
match conv.system().unwrap() {
SystemPrompt::Blocks(blocks) => {
assert_eq!(blocks.len(), 1);
assert!(blocks[0].cache_control.is_some());
}
_ => panic!("Expected Blocks variant"),
}
}
}