Multi-Step Reasoning with Chained Tool Calls — PokéAPI¶
This notebook demonstrates the LLMAgent performing multi-step reasoning by chaining sequential tool calls.
We ask the agent to compare two Pokémon across a specific stat. To answer, it must call get_pokemon twice — once per Pokémon — then synthesise both results into a final answer.
# Uncomment the line below to install `llm-agents-from-scratch` from PyPI
# !pip install llm-agents-from-scratch
Running an Ollama service¶
To execute the code provided in this notebook, you'll need to have Ollama installed on your local machine and have its LLM hosting service running. To download Ollama, follow the instructions found on this page: https://ollama.com/download. After downloading and installing Ollama, you can start a service by opening a terminal and running the command ollama serve.
import os
import shutil
import subprocess
import time
import urllib.error
import urllib.request
def ensure_ollama(host="http://localhost:11434", timeout=15):
"""Start Ollama if not already running and wait until responsive."""
def _up():
try:
urllib.request.urlopen(f"{host}/api/tags", timeout=1)
return True
except (urllib.error.URLError, ConnectionError, TimeoutError):
return False
if _up():
return print(f"✓ Ollama already running at {host}")
# Lightning persistent path first, then standard locations
ollama_path = shutil.which("ollama")
if ollama_path is None:
for candidate in [
"/teamspace/studios/this_studio/.local/bin/ollama",
"/usr/local/bin/ollama",
"/usr/bin/ollama",
]:
if os.path.exists(candidate):
ollama_path = candidate
break
if ollama_path is None:
raise RuntimeError(
"Could not find the ollama binary. Install with: "
"curl -fsSL https://ollama.com/install.sh | sh",
)
print(f"Starting Ollama server ({ollama_path})...")
subprocess.Popen(
[ollama_path, "serve"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
deadline = time.time() + timeout
while time.time() < deadline:
if _up():
return print(f"✓ Ollama up and running at {host}")
time.sleep(0.5)
raise RuntimeError(f"Ollama did not start within {timeout}s")
ensure_ollama()
✓ Ollama already running at http://localhost:11434
Defining the Tool¶
import json
import urllib.error
import urllib.request
from llm_agents_from_scratch.tools.simple_function import SimpleFunctionTool
def get_pokemon(name: str) -> str:
"""Look up a Pokémon by name and return its types and base stats."""
url = f"https://pokeapi.co/api/v2/pokemon/{name.lower().strip()}"
req = urllib.request.Request(
url,
headers={"User-Agent": "llm-agents-from-scratch/1.0"},
)
try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
except urllib.error.HTTPError as e:
if e.code == 404: # noqa: PLR2004
raise ValueError(
f"Pokémon '{name}' not found. "
"Check the spelling and try again.",
) from e
raise
types = [t["type"]["name"] for t in data["types"]]
stats = {s["stat"]["name"]: s["base_stat"] for s in data["stats"]}
return json.dumps({"name": data["name"], "types": types, "stats": stats})
get_pokemon_tool = SimpleFunctionTool(func=get_pokemon)
Example 1 — Comparing Speed¶
The agent needs to look up both Pokémon before it can answer, making two sequential tool calls.
import logging
from llm_agents_from_scratch.logger import enable_console_logging
enable_console_logging(logging.INFO)
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.llms import OllamaLLM
llm = OllamaLLM(model="qwen3:14b", think=False)
agent = LLMAgent(llm=llm, tools=[get_pokemon_tool])
task = Task(
instruction=(
"Which Pokémon has higher base speed: Pikachu or Gengar? "
"Use the get_pokemon tool for each — do not rely on prior knowledge."
),
)
result = await agent.run(task)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Which Pokémon has higher base speed: Pikachu or Gengar? Use the get_pokemon tool for each — do not rely on prior knowledge.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Which Pokémon has higher base speed: Pikachu or Gengar? Use the get_pokemon tool for each — do not rely on prior knowledge.
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the get_pokemon tool for Pikachu and Gengar to retrieve their base speed stats.
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the get_pokemon tool for Pikachu and Gengar to retrieve their base speed stats.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the get_pokemon tool for Pikachu and Gengar to retrieve their base speed stats.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: get_pokemon
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: {"name": "pikachu", "types": ["electric"], "stats": {"hp": 35, "attack": 55, "defense": 40, "special-attack": 50, "special-def...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: get_pokemon
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: {"name": "gengar", "types": ["ghost", "poison"], "stats": {"hp": 60, "attack": 65, "defense": 60, "special-attack": 130, "spec...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: Pikachu has a base speed of 90, while Gengar has a base speed of 110. Therefore, Gengar has a higher base speed.
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: Pikachu has a base speed of 90, while Gengar has a base speed of 110. Therefore, Gengar has a higher base speed.
print(result)
Pikachu has a base speed of 90, while Gengar has a base speed of 110. Therefore, Gengar has a higher base speed.
Example 2 — Comparing Attack and Defense¶
A second comparison task to reinforce the chaining pattern — this time across two different stats.
llm = OllamaLLM(model="qwen3:14b", think=False)
agent = LLMAgent(llm=llm, tools=[get_pokemon_tool])
task = Task(
instruction=(
"Compare Bulbasaur and Charmander. "
"Which has higher attack and which has higher defense? "
"Use the get_pokemon tool for each — do not rely on prior knowledge."
),
)
result = await agent.run(task)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Compare Bulbasaur and Charmander. Which has higher attack and which has higher defense? Use the get_pokemon tool for each — do not re...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compare Bulbasaur and Charmander. Which has higher attack and which has higher defense? Use the get_pokemon tool for each — do not...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the get_pokemon tool for Bulbasaur and Charmander to retrieve their base stats.
INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the get_pokemon tool for Bulbasaur and Charmander to retrieve their base stats.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the get_pokemon tool for Bulbasaur and Charmander to retrieve their base stats.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: get_pokemon
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: {"name": "bulbasaur", "types": ["grass", "poison"], "stats": {"hp": 45, "attack": 49, "defense": 49, "special-attack": 65, "sp...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: get_pokemon
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: {"name": "charmander", "types": ["fire"], "stats": {"hp": 39, "attack": 52, "defense": 43, "special-attack": 60, "special-defe...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I have retrieved the base stats for both Bulbasaur and Charmander. Let me compare their attack and defense stats.
- Bulbasaur's attack...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: I have retrieved the base stats for both Bulbasaur and Charmander. Let me compare their attack and defense stats.
- Bulbasaur's att...[TRUNCATED]
print(result)
I have retrieved the base stats for both Bulbasaur and Charmander. Let me compare their attack and defense stats. - Bulbasaur's attack is 49 and defense is 49. - Charmander's attack is 52 and defense is 43. From this, I can conclude that Charmander has a higher attack, while Bulbasaur has a higher defense.