Asking for a tool
A model that knows about a tool can answer with a request to run it. The agent runs the tool and calls the model again with the result, until the model answers in words.
So far the model could only say it had no way to look an order up. Given a tool, it needs to ask for it and then read what comes back.
from crewai import Agent, Crew, Task
from shop_llm import ShopLLM
from tools import lookup_order
clerk = Agent(
role="Order clerk",
goal="Find the status of customers' orders",
backstory="You can look up any order in the shop's system.",
llm=ShopLLM(model="shop"),
tools=[lookup_order],
)
task = Task(description="Answer the customer: {question}",
expected_output="The order's status in one sentence.", agent=clerk)
crew = Crew(agents=[clerk], tasks=[task])result = crew.kickoff(inputs={"question": "Where is my order A17?"})
print(result.raw)The clerk has the tool, and still cannot look the order up. CrewAI asks a model whether it can make native tool calls, through a method called supports_function_calling. BaseLLM has no such method, so without it, CrewAI describes the tools in the prompt as text and expects the model to write its request in a fixed text format. Lesson 3's model does neither.
A model that asks
import json
import os
import re
from crewai import BaseLLM
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"One new import, json: a tool call carries its arguments as a JSON string, which decide builds with json.dumps. The three settings stay as they were.
class ShopLLM(BaseLLM):
script: list = []
def supports_function_calling(self):
return True
def call(self, messages, tools=None, **kwargs):
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if self.script:
return self.script.pop(0)
names = [t["function"]["name"] for t in tools or []]
return self.decide(messages, names)supports_function_calling returning True makes CrewAI pass the tools to call as a list of schemas. call keeps only their names and hands the choice to a new method, decide. script is a list of fixed replies returned first, in order; from lesson 11 on it lets a lesson force a particular reply.
def decide(self, messages, tools):
last = messages[-1]
if last["role"] == "tool":
return last["content"]
text = last["content"]
orders = re.findall(r"\b[A-Z]\d+\b", text)
wanted = "refund_order" if "refund" in text.lower() else "lookup_order"
matches = [name for name in tools if name.endswith(wanted)]A message with the role tool is a tool's result coming back, and the model answers with it. Otherwise it picks the tool it wants: refund_order when the text mentions a refund, which arrives in lesson 12, and lookup_order otherwise. It matches names by their ending, because tools from an MCP server in lesson 26 carry a prefix.
if orders and matches:
args = json.dumps({"order_id": orders[0]})
return [{"id": f"call_{orders[0]}", "type": "function",
"function": {"name": matches[0], "arguments": args}}]
if orders:
return f"I have no way to look up {orders[0]} yet."
return "Hello. Which order is this about?"A tool call is a dictionary in the format OpenAI's API uses: an id, and the function's name with its arguments as a JSON string. Returning a list of them, instead of text, is how a model asks. Save the file; every lesson from here on imports it.
The loop
result = crew.kickoff(inputs={"question": "Where is my order A17?"})
for message in result.tasks_output[0].messages:
text = message["content"].strip().split("\n")[0]
print(f"{message['role']:<9}", message.get("tool_calls") or text)The system and user messages are lesson 5's. The first assistant message has no text, only a tool call; CrewAI saw it, ran lookup_order, and added the tool message with the result. The model was called again and the last assistant message is its answer. Text with no tool calls is what ended the loop.
Pick one to watch it run, step by step.
The same five messages as a picture. Trace the second question through it to see the round the model skips when there is no order id.
print(crew.kickoff(inputs={"question": "Where is my order B22?"}).raw)
print(crew.kickoff(inputs={"question": "Hello"}).raw)B22 went through the same loop, and the tool's answer became the reply. "Hello" names no order, so the model answered on the first call and no tool ran. The model decides how many times the loop goes round.
- Remove
tools=[lookup_order]from the clerk and ask about A17 again. - Ask "Where are A17 and C40?" and read which order the model picks.
- Print
result.tasks_output[0].messages[2]and find the tool call's id.
This is what real progress feels like.