Task 7 · 8 tasks
Agents as tools
Split the work: an orchestrator routes to a data specialist and a weather specialist.
The “Overloaded Agent” problem
Alice now wants headcounts and the weather for her next business trip. One agent with every tool and every instruction gets confused. Build a small team instead.
Build it
Build the team
Challenge
Create two specialists,
data_agent(database tools plus the approval hook) andweather_agent(forecasts for US locations from the National Weather Service), and an orchestrator that routes to them.Work in
phase1/starter/t7_agents_as_tools.py(look forTODO; an unfinished one prints[starter] TODO …) and run it withuv run bootcamp.py phase1 t7 --starter. The reference solution isphase1/t7_agents_as_tools.py;uv run bootcamp.py phase1 t7runs it.Hint 1
Any function decorated with
@toolis a tool, so a function that runs another agent is one too. This is the “agents as tools” pattern. See Strands: agents as tools.Hint 2
Each specialist gets its own prompt and tools; its docstring tells the orchestrator when to use it. The weather agent's tool,
get_us_forecastinshared/nws_weather.py, is ready-made: import it withWEATHER_PROMPT.sketchread only@tool def data_agent(query: str) -> str: """<when to use me>""" return str(Agent(model=..., system_prompt=..., tools=db_tools, hooks=[...])(query))Solution
phase1/t7_agents_as_tools.pyORCHESTRATOR_PROMPT = """You are CEO Alice's executive assistant at DataStream Corp. Route queries to specialists: - Database queries, employee counts, department stats -> use data_agent - Weather forecasts, temperature, weather conditions (any location) -> use weather_agent. It covers US locations only; when it says it can only look up US weather, pass that sentence on to Alice unchanged. - Simple company questions -> answer directly After the specialists reply, ALWAYS finish with a final answer for Alice that combines every specialist result in your own words. Never end your turn with an empty message. If a specialist reports that it failed, tell Alice which part could not be answered; never invent numbers or use placeholders such as [count]. DataStream Corp is a technology company with Sales, HR, and Engineering departments.""" SPECIALIST_FAILED = "The {specialist} specialist failed: {reason}. No data is available from it for this question." """Returned to the orchestrator instead of "" so a failed specialist is reported, never filled in with guesses.""" def consult(specialist: Agent, query: str, results: list[str], name: str) -> str: """Ask a specialist via `ask_specialist`; an exception or empty answer becomes an explicit failure message. Args: specialist: The specialist agent behind an orchestrator tool. query: The question the orchestrator routed to it. results: Shared list of specialist answers for this turn. name: Short specialist name for the failure message, e.g. "data". Returns: The specialist's answer, or "The <name> specialist failed: ..." so the orchestrator never guesses. """ try: answer = ask_specialist(specialist, query, results).strip() reason = "it returned an empty answer" except Exception as error: answer, reason = "", f"{type(error).__name__}: {error}" if not answer: answer = SPECIALIST_FAILED.format(specialist=name, reason=reason) results.append(answer) return answer def build_orchestrator(db_tools: list, specialist_results: list[str]) -> Agent: """Orchestrator whose two tools are specialist agents; their answers are appended to `specialist_results`.""" @tool def data_agent(query: str) -> str: """Query and analyze the DataStream Corp database: employee counts, department statistics, any SQL question. Args: query: A database or data analysis question. """ specialist = Agent( model=make_model(), system_prompt="You are a data specialist. Query the database to answer questions.", tools=db_tools, hooks=[ApprovalHook()], callback_handler=None, ) return consult(specialist, query, specialist_results, "data") @tool def weather_agent(query: str) -> str: """Get the weather forecast for a US location (National Weather Service); says so for non-US places. Args: query: A weather-related question, including the place. """ specialist = Agent( model=make_model(), system_prompt=WEATHER_PROMPT, tools=[get_us_forecast], callback_handler=None, ) return consult(specialist, query, specialist_results, "weather") return Agent( name="Executive Assistant", model=make_model(), system_prompt=ORCHESTRATOR_PROMPT, tools=[data_agent, weather_agent], )Each specialist tool goes through
consult: if the specialist raises or answers with nothing, the orchestrator gets “The data specialist failed: ...” instead of an empty string, and the prompt tells it to say which part could not be answered. Without that, a model handed an empty tool result tends to make up a plausible number or leave a placeholder like[count].shared/nws_weather.pyUS_ONLY_REPLY = "I can only look up US weather (National Weather Service)." WEATHER_PROMPT = f"""You are a weather specialist for US locations only, using the National Weather Service. - For a place in the United States, call get_us_forecast with its latitude and longitude in decimal degrees, then answer concisely from the forecast periods it returns. - For any place outside the United States, do not call the tool; reply exactly: {US_ONLY_REPLY} - If the tool returns an error, say that the forecast is unavailable; never guess the weather.""" @tool def get_us_forecast(latitude: float, longitude: float) -> str: """Get the National Weather Service forecast for a location in the United States (next few periods). Args: latitude: Latitude of the US location in decimal degrees, e.g. 47.6062 for Seattle. longitude: Longitude of the US location in decimal degrees, e.g. -122.3321 for Seattle. """ try: with make_client() as client: place, periods = point_forecast(client, latitude, longitude) except NotUsLocationError: return f"Error: no NWS forecast for {latitude},{longitude}. {US_ONLY_REPLY}" except httpx.TimeoutException: return f"Error: the National Weather Service did not answer within {TIMEOUT_SECONDS:g}s; try again." except (httpx.HTTPError, KeyError, ValueError) as error: return f"Error: the National Weather Service request failed ({type(error).__name__}: {error})." lines = [format_period(period) for period in periods[:FORECAST_PERIODS]] return "\n".join([f"NWS forecast for {place}:", *lines])The weather tool is purpose-built: it calls
/points/{lat},{lon}and the forecast URL itself and returns four short lines (name, temperature, wind, short forecast). Handing the model the raw NWS GeoJSON through a generic HTTP tool cost 8K-75K input tokens per question; this costs a fraction of that. The NWS only covers the US, so the prompt gives the model one exact sentence for other places instead of letting it improvise a forecast.Run it
terminal · your starter fileuv run bootcamp.py phase1 t7 --starter "How many employees are in Engineering?" uv run bootcamp.py phase1 t7 --starter "What is the weather in Seattle?" uv run bootcamp.py phase1 t7 --starter "How many employees do we have and what is the weather in New York?" uv run bootcamp.py phase1 t7 --starter "What is the weather in London?"terminal · reference solutionuv run bootcamp.py phase1 t7 "How many employees are in Engineering?" uv run bootcamp.py phase1 t7 "What is the weather in Seattle?" uv run bootcamp.py phase1 t7 "How many employees do we have and what is the weather in New York?" uv run bootcamp.py phase1 t7 "What is the weather in London?"Experiments
- Watch the
[tool] data_agent: .../[tool] weather_agent: ...lines, followed by the specialists' own[tool] query_db/[tool] get_us_forecastlines: what question does the orchestrator actually pass down? - Try
MODEL_ID=claude-sonnet-5-5for the combined question, then checkuv run bootcamp.py llm. Was the better routing worth the spend? - Add a third specialist, for example one that returns a
DepartmentReport.
- Watch the
Check your work
Phase 1 has no automated test: you check it by running the task and looking for the result below.
Database questions go to data_agent, weather questions to weather_agent, and the combined question calls both and merges the answers.
The LiteLLM gateway already retries a model reply that carries no answer and then falls back from gpt-6-luna to claude-sonnet-5-5, so empty turns are rare. A specialist that still fails shows up as “The data specialist failed: ...” in the answer instead of made-up numbers. If the orchestrator itself ends with an empty answer, the script prints [fallback] Empty final answer (try MODEL_ID=claude-sonnet-5-5); last specialist result: followed by that result. If it happens often, set MODEL_ID=claude-sonnet-5-5 in .env and re-run.
Under the hood
A @tool is a function with a schema derived from its signature and docstring, so a function that runs another agent is a tool too. Each specialist has its own prompt, tools and context, which keeps prompts short and failures contained. This orchestrator is what you deploy to AgentCore Runtime in Phase 2, Task 4.