Skip to main content

Multi-Turn conversations with tools

📖 Lesson content

Summary

Building multi-turn conversations with tool use requires handling different response types from Claude. When Claude responds, it might need to use a tool, or it might provide a direct answer. Your code needs to handle both scenarios gracefully.

The Problem with Simple Tool Integration

If you just add tool results to every conversation, you'll run into issues. When Claude answers a simple question like "What is 1+1?", it doesn't need any tools. But if your code always tries to process tool results, you'll end up adding empty messages to your conversation history.

The solution is to check the stop_reason that comes back with every Claude response. This tells you why Claude stopped generating - whether it finished naturally or because it wants to use a tool.

Stop Reasons

Claude can stop for several reasons:

  • "tool_use" - The model wants to call a tool
  • "end_turn" - Model finished generating its response
  • "max_tokens" - Hit the output limit
  • "stop_sequence" - Encountered a stop sequence you provided

Improving the Chat Function

First, update your chat function to return more information. Instead of just returning text and parts separately, return a dictionary with everything you need:

def chat(messages, tools=None, system=None, **kwargs):
    # ... existing code ...
    
    return {
        "parts": parts,
        "stop_reason": response["stopReason"],
        "text": "\n".join([p["text"] for p in parts if "text" in p])
    }

This approach extracts all text content from the response parts, which is more robust than assuming the first part is always text.

Building a Conversation Loop

Create a function that handles the full conversation flow:

def run_conversation(messages):
    while True:
        result = chat(messages, tools=[get_current_datetime_schema])
        
        add_assistant_message(messages, result["parts"])
        print(result["text"])
        
        if result["stop_reason"] != "tool_use":
            break
            
        tool_result_parts = run_tools(result["parts"])
        add_user_message(messages, tool_result_parts)
    
    return messages

This loop continues until Claude stops for a reason other than tool use. Each iteration:

  1. Sends the current messages to Claude
  2. Adds Claude's response to the message history
  3. Checks if Claude wants to use a tool
  4. If so, runs the tools and adds results back to the conversation
  5. If not, exits the loop

tool_use

iterate

end_turn / other

User message

Send messages → Bedrock Converse

Claude response + stop_reason

stop_reason == tool_use?

run_tools — append results

Final answer to user

Testing the Implementation

This approach handles both tool-requiring and simple questions:


messages = []
add_user_message(messages, "What time is it?")
run_conversation(messages)

messages = []
add_user_message(messages, "What is 1+1?")
run_conversation(messages)

For time questions, Claude will use the datetime tool. For math questions, it responds directly without any tool calls. The conversation loop adapts automatically based on Claude's stop reason.

This pattern scales well when you add more tools - the same loop handles any combination of tool use and direct responses, making your conversational AI more robust and natural.

🔁 Related lessons

📚 Source & attribution

Was this lesson helpful?

Feedback / ReportSpotted an issue or have an improvement idea?