Skip to main content

agentik_sdk/tools/
conversation.rs

1//! High-level tool conversation management.
2//!
3//! This module provides abstractions for managing multi-turn conversations
4//! that involve tool use, handling the back-and-forth between Claude and tools.
5
6use std::sync::Arc;
7use crate::client::Anthropic;
8use crate::types::{Message, ToolChoice, ToolResult, MessageCreateBuilder};
9use super::{ToolRegistry, ToolExecutor, ToolExecutionConfig, ToolOperationResult, ToolError};
10
11/// High-level tool conversation manager.
12///
13/// This provides a simplified interface for conducting conversations with Claude
14/// that involve tool use, automatically handling tool execution and conversation flow.
15pub struct ToolConversation {
16    /// The Anthropic client for API calls.
17    client: Arc<Anthropic>,
18    
19    /// Tool registry for executing tools.
20    registry: Arc<ToolRegistry>,
21    
22    /// Tool executor for advanced execution features.
23    executor: ToolExecutor,
24    
25    /// Configuration for the conversation.
26    config: ConversationConfig,
27}
28
29/// Configuration for tool conversations.
30#[derive(Debug, Clone)]
31pub struct ConversationConfig {
32    /// Maximum number of conversation turns.
33    pub max_turns: usize,
34    
35    /// Model to use for the conversation.
36    pub model: String,
37    
38    /// Maximum tokens per response.
39    pub max_tokens: u32,
40    
41    /// Tool choice strategy.
42    pub tool_choice: Option<ToolChoice>,
43    
44    /// Whether to automatically execute tools.
45    pub auto_execute_tools: bool,
46    
47    /// Tool execution configuration.
48    pub execution_config: ToolExecutionConfig,
49}
50
51impl Default for ConversationConfig {
52    fn default() -> Self {
53        Self {
54            max_turns: 10,
55            model: "claude-3-5-sonnet-latest".to_string(),
56            max_tokens: 1024,
57            tool_choice: Some(ToolChoice::Auto),
58            auto_execute_tools: true,
59            execution_config: ToolExecutionConfig::default(),
60        }
61    }
62}
63
64impl ToolConversation {
65    /// Create a new tool conversation.
66    pub fn new(client: Arc<Anthropic>, registry: Arc<ToolRegistry>) -> Self {
67        let executor = ToolExecutor::new(registry.clone());
68        Self {
69            client,
70            registry: registry.clone(),
71            executor,
72            config: ConversationConfig::default(),
73        }
74    }
75
76    /// Create a new tool conversation with custom configuration.
77    pub fn with_config(
78        client: Arc<Anthropic>,
79        registry: Arc<ToolRegistry>,
80        config: ConversationConfig,
81    ) -> Self {
82        let executor = ToolExecutor::with_config(registry.clone(), config.execution_config.clone());
83        Self {
84            client,
85            registry: registry.clone(),
86            executor,
87            config,
88        }
89    }
90
91    /// Start a conversation with an initial user message.
92    ///
93    /// This method initiates a conversation and returns the first response from Claude.
94    /// If Claude uses tools, they will be automatically executed if `auto_execute_tools` is enabled.
95    pub async fn start(&self, user_message: impl Into<String>) -> ToolOperationResult<Message> {
96        let tools = self.registry.get_tool_definitions();
97        
98        let mut builder = MessageCreateBuilder::new(&self.config.model, self.config.max_tokens)
99            .user(user_message.into());
100
101        // Add tools if available
102        if !tools.is_empty() {
103            builder = builder.tools(tools);
104            
105            if let Some(ref tool_choice) = self.config.tool_choice {
106                builder = builder.tool_choice(tool_choice.clone());
107            }
108        }
109
110        let message = self.client.messages()
111            .create(builder.build())
112            .await
113            .map_err(|e| ToolError::ExecutionFailed { source: e.into() })?;
114
115        Ok(message)
116    }
117
118    /// Continue a conversation by processing tool uses and getting the next response.
119    ///
120    /// This method takes a message that may contain tool use requests, executes the tools,
121    /// and returns Claude's response incorporating the tool results.
122    pub async fn continue_with_tools(&self, message: &Message) -> ToolOperationResult<Option<Message>> {
123        let tool_uses = self.executor.extract_tool_uses(message);
124        
125        if tool_uses.is_empty() {
126            return Ok(None);
127        }
128
129        if !self.config.auto_execute_tools {
130            // Return without executing tools - let the caller handle execution
131            return Ok(None);
132        }
133
134        // Execute all tools
135        let tool_results = self.executor.execute_multiple(&tool_uses).await;
136        
137        // Convert execution results to tool results
138        let mut results = Vec::new();
139        for (tool_use, result) in tool_uses.iter().zip(tool_results.iter()) {
140            match result {
141                Ok(tool_result) => results.push(tool_result.clone()),
142                Err(error) => {
143                    results.push(ToolResult::error(
144                        tool_use.id.clone(),
145                        format!("Tool execution failed: {}", error),
146                    ));
147                }
148            }
149        }
150
151        // Create a follow-up message with tool results
152        use crate::types::messages::{MessageContent, ContentBlockParam};
153        
154        // Convert tool results to content blocks
155        let tool_result_blocks: Vec<ContentBlockParam> = results.into_iter().map(|result| {
156            // Convert ToolResultContent to String for ContentBlockParam::ToolResult
157            let content_string = match result.content {
158                crate::types::ToolResultContent::Text(text) => Some(text),
159                crate::types::ToolResultContent::Json(json) => Some(json.to_string()),
160                crate::types::ToolResultContent::Blocks(blocks) => {
161                    // Convert blocks to a simple text representation
162                    let text_parts: Vec<String> = blocks.into_iter().map(|block| {
163                        match block {
164                            crate::types::ToolResultBlock::Text { text } => text,
165                            crate::types::ToolResultBlock::Image { .. } => "[Image]".to_string(),
166                        }
167                    }).collect();
168                    Some(text_parts.join("\n"))
169                }
170            };
171            
172            ContentBlockParam::ToolResult {
173                tool_use_id: result.tool_use_id,
174                content: content_string,
175                is_error: result.is_error,
176            }
177        }).collect();
178        
179        let mut builder = MessageCreateBuilder::new(&self.config.model, self.config.max_tokens)
180            .user(MessageContent::Blocks(tool_result_blocks));
181            
182        // Add tools again for potential follow-up tool use
183        let tools = self.registry.get_tool_definitions();
184        if !tools.is_empty() {
185            builder = builder.tools(tools);
186            
187            if let Some(ref tool_choice) = self.config.tool_choice {
188                builder = builder.tool_choice(tool_choice.clone());
189            }
190        }
191        
192        let next_message = self.client.messages()
193            .create(builder.build())
194            .await
195            .map_err(|e| ToolError::ExecutionFailed { source: e.into() })?;
196
197        Ok(Some(next_message))
198    }
199
200    /// Execute a complete conversation until completion or max turns reached.
201    ///
202    /// This method manages the entire conversation flow, automatically executing tools
203    /// and continuing the conversation until Claude provides a final response.
204    pub async fn execute_until_complete(&self, initial_message: impl Into<String>) -> ToolOperationResult<Message> {
205        let mut current_message = self.start(initial_message).await?;
206        let mut turn_count = 1;
207
208        while turn_count < self.config.max_turns {
209            match self.continue_with_tools(&current_message).await? {
210                Some(next_message) => {
211                    current_message = next_message;
212                    turn_count += 1;
213                }
214                None => {
215                    // No more tools to execute, conversation is complete
216                    break;
217                }
218            }
219        }
220
221        if turn_count >= self.config.max_turns {
222            return Err(ToolError::ExecutionFailed {
223                source: "Conversation exceeded maximum turns".to_string().into(),
224            });
225        }
226
227        Ok(current_message)
228    }
229
230    /// Get the tool registry.
231    pub fn registry(&self) -> &Arc<ToolRegistry> {
232        &self.registry
233    }
234
235    /// Get the tool executor.
236    pub fn executor(&self) -> &ToolExecutor {
237        &self.executor
238    }
239
240    /// Get the conversation configuration.
241    pub fn config(&self) -> &ConversationConfig {
242        &self.config
243    }
244
245    /// Update the conversation configuration.
246    pub fn set_config(&mut self, config: ConversationConfig) {
247        self.config = config;
248        self.executor.set_config(self.config.execution_config.clone());
249    }
250
251
252}
253
254/// Builder for creating conversation configurations.
255pub struct ConversationConfigBuilder {
256    config: ConversationConfig,
257}
258
259impl ConversationConfigBuilder {
260    /// Create a new configuration builder.
261    pub fn new() -> Self {
262        Self {
263            config: ConversationConfig::default(),
264        }
265    }
266
267    /// Set the maximum number of conversation turns.
268    pub fn max_turns(mut self, max_turns: usize) -> Self {
269        self.config.max_turns = max_turns;
270        self
271    }
272
273    /// Set the model to use.
274    pub fn model(mut self, model: impl Into<String>) -> Self {
275        self.config.model = model.into();
276        self
277    }
278
279    /// Set the maximum tokens per response.
280    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
281        self.config.max_tokens = max_tokens;
282        self
283    }
284
285    /// Set the tool choice strategy.
286    pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
287        self.config.tool_choice = Some(tool_choice);
288        self
289    }
290
291    /// Enable or disable automatic tool execution.
292    pub fn auto_execute_tools(mut self, enabled: bool) -> Self {
293        self.config.auto_execute_tools = enabled;
294        self
295    }
296
297    /// Set the tool execution configuration.
298    pub fn execution_config(mut self, config: ToolExecutionConfig) -> Self {
299        self.config.execution_config = config;
300        self
301    }
302
303    /// Build the configuration.
304    pub fn build(self) -> ConversationConfig {
305        self.config
306    }
307}
308
309impl Default for ConversationConfigBuilder {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_conversation_config_builder() {
321        let config = ConversationConfigBuilder::new()
322            .max_turns(5)
323            .model("claude-3-5-sonnet-latest")
324            .max_tokens(2048)
325            .tool_choice(ToolChoice::Any)
326            .auto_execute_tools(false)
327            .build();
328
329        assert_eq!(config.max_turns, 5);
330        assert_eq!(config.model, "claude-3-5-sonnet-latest");
331        assert_eq!(config.max_tokens, 2048);
332        assert_eq!(config.tool_choice, Some(ToolChoice::Any));
333        assert!(!config.auto_execute_tools);
334    }
335
336    #[test]
337    fn test_default_config() {
338        let config = ConversationConfig::default();
339        assert_eq!(config.max_turns, 10);
340        assert_eq!(config.model, "claude-3-5-sonnet-latest");
341        assert_eq!(config.max_tokens, 1024);
342        assert_eq!(config.tool_choice, Some(ToolChoice::Auto));
343        assert!(config.auto_execute_tools);
344    }
345}