CrewAICrewAI 1.15 · Python 3.10 to 3.13
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
32 small wins to finish your pathNext lesson

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.

Examplecrew.py
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])
Example
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

Exampleshop_llm.py, the imports now
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.

Exampleshop_llm.py, the new class
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.

Exampleshop_llm.py, the start of decide
    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.

Exampleshop_llm.py, the end of decide
        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

Example
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.

The tool-call loop, one round
tool callrunsno tool callsThe task promptthe customer's questionShopLLM.decidetext or a tool calllookup_orderyour Python functionTool messagethe tool's answerFinal answertext, no tool calls
Hover or tap a piece to see what it is and which lesson built it.
Trace 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.

Example
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.

Try it yourself
  • 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.