Build a simplified agent that orchestrates multiple tools to fulfill a user request. This agent plans a simple outing by fetching information, making a suggestion based on that information, and finding a related amenity.Our goal is to illustrate how an agent can execute a sequence of tool calls, where the output of one tool often becomes the input for the next. This demonstrates the core concepts of multi-step execution and dependency management discussed earlier in this chapter.Scenario: The Trip Planner AgentImagine a user wants help planning a local activity. They might ask something like, "What's a good activity for this Saturday in San Francisco, and where can I eat nearby?" To answer this, our agent will need to:Determine the weather for the specified location and date.Suggest an activity suitable for that weather.Find a restaurant near the suggested activity.This clearly requires a sequence of operations, making it a good candidate for multi-tool orchestration.Defining the ToolsFor this exercise, we'll define three Python functions, each representing a distinct tool. In an application, these tools might interact with external APIs (as covered in Chapter 4), but here we'll use simplified implementations to keep the focus on orchestration.get_weather_forecast: Takes a location and date, and returns a dictionary with the weather condition and temperature.suggest_activity: Takes a weather condition and suggests an activity.find_restaurant: Takes a city and an activity descriptor (like "near the park"), and returns a restaurant suggestion.Here are the Python implementations for these tools. Notice their input parameters and the structure of their return values. Clear input/output schemas are important for reliable tool use, as discussed in Chapter 1.# tool_definitions.py def get_weather_forecast(location: str, date: str) -> dict: """ Simulates fetching a weather forecast. In a real tool, this would call a weather API. """ print(f"TOOL CALLED: get_weather_forecast(location='{location}', date='{date}')") if location.lower() == "san francisco": if "saturday" in date.lower() or "sunday" in date.lower(): # Simulate weekend weather return {"city": location, "condition": "Sunny", "temperature_celsius": 22} else: return {"city": location, "condition": "Foggy", "temperature_celsius": 18} elif location.lower() == "london": return {"city": location, "condition": "Cloudy", "temperature_celsius": 15} else: return {"city": location, "condition": "Partly Cloudy", "temperature_celsius": 20} def suggest_activity(weather_condition: str) -> dict: """ Suggests an activity based on the weather condition. """ print(f"TOOL CALLED: suggest_activity(weather_condition='{weather_condition}')") if weather_condition == "Sunny": return {"activity_type": "Outdoor", "suggestion": "Go for a hike in a nearby park"} elif weather_condition == "Cloudy" or weather_condition == "Partly Cloudy": return {"activity_type": "Indoor/Outdoor", "suggestion": "Visit a local museum or a covered market"} elif weather_condition == "Foggy": return {"activity_type": "Indoor", "suggestion": "Explore an art gallery or enjoy a cozy cafe"} else: # Default for other conditions return {"activity_type": "Flexible", "suggestion": "Check out local events"} def find_restaurant(city: str, activity_type: str, preference: str = "near activity") -> dict: """ Simulates finding a restaurant based on city, activity type, and preference. 'preference' could be used to find something 'near activity' or a specific cuisine. """ print(f"TOOL CALLED: find_restaurant(city='{city}', activity_type='{activity_type}', preference='{preference}')") if city.lower() == "san francisco": if activity_type == "Outdoor": return {"name": "The Green Eatery", "cuisine": "Californian", "vicinity": "near Golden Gate Park"} elif activity_type == "Indoor": return {"name": "City Lights Cafe", "cuisine": "Italian", "vicinity": "Downtown"} else: return {"name": "The Bay Diner", "cuisine": "American", "vicinity": "Fisherman's Wharf"} elif city.lower() == "london": return {"name": "The Thames Bistro", "cuisine": "British", "vicinity": "near South Bank"} else: return {"name": "Local Gem Restaurant", "cuisine": "Varied", "vicinity": "city center"} Orchestrating the Tool CallsWith our tools defined, we can now outline the agent's logic. The agent will receive an initial query, then call the tools in a specific sequence, using the output from one tool as input for the next.Here's a visual representation of the planned tool execution flow:digraph G { rankdir=TB; bgcolor="transparent"; node [shape=box, style="filled", fontname="Arial", margin=0.2]; edge [fontname="Arial", fontsize=10]; Start [label="User Request:\n'Plan my Saturday\nin San Francisco'", fillcolor="#bac8ff", shape=ellipse]; GetWeather [label="Tool 1: get_weather_forecast\nInput: 'San Francisco', 'Saturday'", fillcolor="#96f2d7"]; SuggestActivity [label="Tool 2: suggest_activity\nInput: (Weather Condition from Tool 1)", fillcolor="#96f2d7"]; FindRestaurant [label="Tool 3: find_restaurant\nInput: (City from Tool 1),\n(Activity Type from Tool 2)", fillcolor="#96f2d7"]; Synthesize [label="Agent: Formulate Final Response", fillcolor="#bac8ff", shape=ellipse]; Start -> GetWeather [label="location, date"]; GetWeather -> SuggestActivity [label="weather_info (condition)"]; SuggestActivity -> FindRestaurant [label="activity_info (type)"]; GetWeather -> FindRestaurant [label="weather_info (city)"]; /* City info might also come from weather tool output */ FindRestaurant -> Synthesize [label="restaurant_info"]; SuggestActivity -> Synthesize [label="activity_info"]; GetWeather -> Synthesize [label="weather_info"]; }This diagram illustrates the flow of information. The user's request initiates the process. The output of get_weather_forecast (e.g., "Sunny") is used by suggest_activity. Both get_weather_forecast (for the city) and suggest_activity (for the type of activity) provide inputs to find_restaurant. Finally, the agent combines all gathered information to create a response.Now, let's implement this orchestration logic in a Python function that simulates our agent.# agent_orchestrator.py from tool_definitions import get_weather_forecast, suggest_activity, find_restaurant def trip_planner_agent(query_location: str, query_date: str): """ Orchestrates tool calls to plan a trip based on user query. """ print(f"\nAGENT: Received request to plan for {query_location} on {query_date}.") # Step 1: Get the weather forecast print("AGENT: Step 1 - Fetching weather forecast...") weather_info = get_weather_forecast(location=query_location, date=query_date) if not weather_info: print("AGENT: Could not retrieve weather information. Aborting plan.") return "I'm sorry, I couldn't get the weather information for your request." print(f"AGENT: Weather in {weather_info.get('city', 'N/A')} is {weather_info.get('condition', 'N/A')}, {weather_info.get('temperature_celsius', 'N/A')}°C.") # Step 2: Suggest an activity based on the weather # This step depends on the output of get_weather_forecast print("\nAGENT: Step 2 - Suggesting an activity...") activity_info = suggest_activity(weather_condition=weather_info.get('condition')) if not activity_info: print("AGENT: Could not suggest an activity. Aborting plan.") return "I'm sorry, I couldn't come up with an activity suggestion based on the weather." print(f"AGENT: Suggested activity type: {activity_info.get('activity_type', 'N/A')}, Suggestion: {activity_info.get('suggestion', 'N/A')}.") # Step 3: Find a restaurant # This step depends on the city (from weather_info) and activity_type (from activity_info) print("\nAGENT: Step 3 - Finding a restaurant...") restaurant_info = find_restaurant(city=weather_info.get('city'), activity_type=activity_info.get('activity_type')) if not restaurant_info: print("AGENT: Could not find a restaurant. Proceeding with partial plan.") # We could decide to return a partial plan or fail. Here, we continue. restaurant_suggestion_text = "I couldn't find a specific restaurant suggestion at this time." else: restaurant_suggestion_text = ( f"For a meal, you could try {restaurant_info.get('name', 'a local spot')} " f"({restaurant_info.get('cuisine', 'various cuisines')}) " f"located {restaurant_info.get('vicinity', 'nearby')}." ) print(f"AGENT: Restaurant found: {restaurant_info.get('name', 'N/A')}.") # Step 4: Synthesize the final response for the user print("\nAGENT: Step 4 - Synthesizing the final plan...") final_plan = ( f"Okay, here's a plan for your {query_date} in {query_location}:\n" f"- The weather is expected to be {weather_info.get('condition')} with a temperature of around {weather_info.get('temperature_celsius')}°C.\n" f"- Based on that, I suggest you: {activity_info.get('suggestion')}.\n" f"- {restaurant_suggestion_text}" ) print("\nAGENT: Final plan generated:") print(final_plan) return final_plan # Let's run our agent if __name__ == "__main__": user_location = "San Francisco" user_date = "Saturday" plan = trip_planner_agent(query_location=user_location, query_date=user_date) print("\n--- Example with another location ---") user_location_2 = "London" user_date_2 = "next Tuesday" plan_2 = trip_planner_agent(query_location=user_location_2, query_date=user_date_2) When you run agent_orchestrator.py, you'll see output logs showing each tool being called and the agent's internal reasoning at each step:AGENT: Received request to plan for San Francisco on Saturday. AGENT: Step 1 - Fetching weather forecast... TOOL CALLED: get_weather_forecast(location='San Francisco', date='Saturday') AGENT: Weather in San Francisco is Sunny, 22°C. AGENT: Step 2 - Suggesting an activity... TOOL CALLED: suggest_activity(weather_condition='Sunny') AGENT: Suggested activity type: Outdoor, Suggestion: Go for a hike in a nearby park. AGENT: Step 3 - Finding a restaurant... TOOL CALLED: find_restaurant(city='San Francisco', activity_type='Outdoor', preference='near activity') AGENT: Restaurant found: The Green Eatery. AGENT: Step 4 - Synthesizing the final plan... AGENT: Final plan generated: Okay, here's a plan for your Saturday in San Francisco: - The weather is expected to be Sunny with a temperature of around 22°C. - Based on that, I suggest you: Go for a hike in a nearby park. - For a meal, you could try The Green Eatery (Californian) located near Golden Gate Park. --- Example with another location --- AGENT: Received request to plan for London on next Tuesday. AGENT: Step 1 - Fetching weather forecast... TOOL CALLED: get_weather_forecast(location='London', date='next Tuesday') AGENT: Weather in London is Cloudy, 15°C. AGENT: Step 2 - Suggesting an activity... TOOL CALLED: suggest_activity(weather_condition='Cloudy') AGENT: Suggested activity type: Indoor/Outdoor, Suggestion: Visit a local museum or a covered market. AGENT: Step 3 - Finding a restaurant... TOOL CALLED: find_restaurant(city='London', activity_type='Indoor/Outdoor', preference='near activity') AGENT: Restaurant found: The Thames Bistro. AGENT: Step 4 - Synthesizing the final plan... AGENT: Final plan generated: Okay, here's a plan for your next Tuesday in London: - The weather is expected to be Cloudy with a temperature of around 15°C. - Based on that, I suggest you: Visit a local museum or a covered market. - For a meal, you could try The Thames Bistro (British) located near South Bank.Analyzing the OrchestrationThis hands-on exercise demonstrates several important aspects of tool orchestration:Multi-Step Execution: The overall task ("plan a trip") was broken down into a sequence of smaller, manageable steps, each handled by a specific tool.Dependency Management: The execution flow explicitly managed dependencies. The suggest_activity tool required the condition from get_weather_forecast. The find_restaurant tool used the city from get_weather_forecast and the activity_type from suggest_activity. This chaining of inputs and outputs is fundamental to complex tool use.Tool Selection (Simplified): In this example, the sequence of tools was hardcoded into our trip_planner_agent function. In more advanced agents, an LLM would dynamically decide which tool to call next based on the conversation history, the current goal, and the descriptions of available tools (as discussed in "Agent-Driven Tool Selection Mechanisms"). Our explicit sequence serves to clearly illustrate the orchestration flow.Data Transformation/Aggregation: The agent doesn't just pass data blindly. It extracts specific pieces of information from tool outputs (e.g., weather_info.get('condition')) to use as inputs for subsequent tools or for constructing the final response.Further StepsWhile this example is simplified, it forms the basis for more complex orchestrations. Here are a few points to consider for more advanced scenarios:Error Handling: Our agent has basic checks for missing tool outputs. Agents need more sophisticated error handling, such as retrying a tool call, trying an alternative tool, or asking the user for clarification if a tool fails or returns unexpected results (see "Recovering from Failures in Tool Chains").Conditional Logic: We could introduce conditional paths. For example, if the weather is "Rainy," the agent might skip suggesting an outdoor activity or explicitly use a tool designed for finding indoor activities. This relates to "Conditional Tool Execution Logic."Parallel Execution: For tasks where multiple independent pieces of information are needed, an agent might execute some tool calls in parallel to save time (see "Implementing Sequential and Parallel Tool Use"). For instance, if we needed to find opening hours for a venue and book transport simultaneously, those could potentially be parallel operations if they don't depend on each other.This hands-on exercise has given you a practical look at how an agent can coordinate multiple tools to achieve a complex goal. By defining clear tools and orchestrating their execution, you can significantly expand the capabilities of your LLM agents. As you build more sophisticated agents, the principles of sequential execution, dependency management, and (eventually) dynamic tool selection will be central to their design. Try modifying the existing tools, or adding a new one (e.g., a get_event_listings tool), and think about how you would integrate it into the agent's orchestration logic.