IV. Tool calling

Tool calling

Introduction to the tool calling concept

Letting an LLM call a tool is the most powerful thing an agent can do. But what is a “tool”? Simply put, it’s a Python function. This function might be simple or wrap complex logic, it doesn’t matter. What matters is that the LLM can call it with the right parameters. This allows LLMs to interact with traditional, deterministic code. For example, imagine building an LLM-powered calculator. You shouldn’t rely on the LLM to do the math itself. It may handle basic arithmetic but will fail on more complex expressions. Instead, let the CPU do the math. The LLM’s job is to break down the equation and call the appropriate tools to compute each step.

In what way is Yacana different from other frameworks?

Other frameworks assign their tools to the agent during its initialization. This creates a hard link between the tools and the agents. In our opinion, this implementation tends to confuse the agent because it's getting access to many tools that may not be relevant to the immediate task it is given. In Yacana tools are only available at the Task level. Thus no noise is generated before having to solve a particular task. The tool is made available to the LLM only when it's needed and not before. Also, the Agent doesn't keep the memory of the available tools so it won't be tempted to use them elsewhere, where it wouldn't be appropriate.


Understanding the underlying mechanism of tool calling in LLMs

A side note for beginners...


Show section...

You might wonder: how can a text-generating AI call a Python function? Well, it can't. Not directly.


When we talk about tool calling (sometimes called function calling), we’re referring to a specific process: the LLM outputs a well-structured JSON object that describes a function to call, including its name and the parameters to use. These parameters, and their values, are generated by the model.


Since LLMs don’t normally speak JSON, it’s easy to detect when they’re trying to call a function. If the output matches the expected JSON structure, we parse it and trigger the corresponding Python function with the provided parameters. Otherwise, the LLM’s response is treated like any regular reply, and the workflow continues.


But how does the model know which functions it can call, and how to call them? Before the LLM can make any function call, you must send it a message containing two things:

  • A task or instruction
  • A list of available tools

This tool list is itself a JSON array. It includes each function’s name, description, and the parameters it accepts, including the expected types.
The types are important: passing "4" (a string) instead of 4 (an integer) can break the call or cause bugs. Some parameters may also be marked as optional, allowing the LLM to skip the function call if it feels it can answer the task directly.


The most widely used standard for function calling comes from OpenAI. But not all LLMs can follow it. Why? Because function calling requires specialized training.
Most LLMs are trained to follow instructions in a chat format, usually with USER and ASSISTANT messages. But for tool use, a third message type — TOOL — is needed, and only certain models (like Llama, Gemma, or DeepSeek) are fine-tuned to handle it correctly.


Unfortunately, the OpenAI JSON format can be quite heavy, and smaller models (like basic 7B or 8B models) often struggle with it. That’s why Yacana includes its own lightweight function-calling format. It’s easier for smaller models to handle and uses percussive maintenance to nudge the model into outputting valid JSON.


As a result, Yacana enables tool calling with almost any LLM, while still supporting the OpenAI format when needed.


Calling a tool

Let's write our first tool call to perform a simple addition!


First, let's define our tool:


def adder(first_number: int, second_number: int) -> int:
    print(f"Tool adder was called with param {first_number}) ({type(first_number)} and {second_number} ({type(second_number)})")
    return first_number + second_number
					

What do we have here?

  • The name of the function must be relevant to what the function does. Here the function performs an addition so we'll call it adder ;
  • The same thing goes for the parameters. The name you choose is very important as it will help the LLM to know what value to give this parameter ;
  • Duck typing the prototype is very important! You must set the type of each parameter and also the return type of the function ;
  • We perform the operation between the two parameters and return the final result ;

⚠️ Be aware that whatever the return of your function, Yacana will cast it to string using the built-in str(...) function. LLMs can only understand text so make sure that whatever you send back can be cast correctly (override the str if needed).

Let's create a Tool instance using the Yacana Tool constructor. It takes a name (no space), a description, and a reference to the actual function.
I can only emphasize once more on the importance of providing an accurate description.


adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder)
					

Now let's assign our adder_tool to a Task. How to do that? It's simple, the Task() class takes an optional tools=[] parameter. It's an array so be sure not to forget those []!


Task(f"What's 2+2 ?", agent1, tools=[adder_tool]).solve()
					

Full code:


from yacana import OllamaAgent, Tool, Task

def adder(first_number: int, second_number: int) -> int:
    print(f"Tool adder was called with param {first_number} {type(first_number)} and {second_number} ({type(second_number)})")
    return first_number + second_number

agent1 = OllamaAgent("Ai assistant", "llama3.1:8b")

adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder)
result: str = Task(f"What's 2+2 ?", agent1, tools=[adder_tool]).solve().content

print("Equation result = ", result)
					

▶️ Output:


INFO: [PROMPT][To: Ai assistant]: I give you the following tool definition that you must use to fulfill a future task: adder(first_number: int, second_number: int) -> int - Adds two numbers and returns the result. Please acknowledge the given tool.

INFO: [AI_RESPONSE][From: Ai assistant]: Thank you for providing me with the `adder` tool definition! I understand that this is a function that takes two integer arguments (`first_number` and `second_number`) and returns an integer result, which represents the sum of these two input numbers.

I will keep this in mind as we progress through our tasks. Please go ahead and give me the next instruction or task to complete!

INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect the arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: Thank you for clarifying how to structure the tool calls.

In that case, I will extract each parameter and use it as a JSON key. For the `adder` tool, I will structure the tool call as follows:

{'first_number': 3, 'second_number': 5}

Please let me know what's next!

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. Use the tool at your disposition to solve the task by outputting as JSON the correct arguments. In return you will get an answer from the tool. The task is:
What's 2+2 ?

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": "2", "second_number": "2"}

Tool adder was called with param '2' (<class 'str'>) and '2' (<class 'str'>)
Equation result =  22
					

The multiple INFO logs you are seeing here is Yacana doing its magic to make the LLM call the tool.

Unfortunately, even though the tool is indeed called, getting a correct result failed spectacularly! ^^
Is 2 + 2 = 22? No, I don't think so. Can you find out what went wrong?


When looking at the logs we can see that the tool was called with the following JSON: {"first_number": "2", "second_number": "2"}. The values are of type string. Later confirmed by the print() inside the tool itself: param '2' ('str') and '2' ('str').
So instead of having integers, we got strings and what's the result of "2" + "2" in Python?
Not "4" but "22" (concatenation of strings). Bummer! ^^

Fortunately, we can fix this easily in several ways.


Improving tool-calling results

As you saw in the previous adder example we ran into trouble with the 2 + 2 call sent as a string. Let's fix that.


Adding validation inside the Tool

Since LLMs are not deterministic, we can never be certain of the values they’ll send to our tools. That’s why you should think of a tool the same way you would a web server route,it requires server-side validation. Your tool must verify the input it receives and raise an error if the data is invalid.

This involves adding strict validation checks to your tool. When the LLM sends an incorrect value, an error will be raised, not just any error, but a ToolError(...). Yacana will catch this exception and notify the LLM that something went wrong during the tool call.
It’s crucial to provide clear and specific error messages in the exception, as the LLM will use that message to adjust its next attempt.


Let's upgrade our adder tool!


from yacana import OllamaAgent, Tool, Task, ToolError


def adder(first_number: int, second_number: int) -> int:
    print(f"Tool adder was called with param {first_number} {type(first_number)} and {second_number} ({type(second_number)})")
    # Adding type validation
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
					

We added type validation on both parameters. But you should also check for None values, etc. As I said. Think of this as server-side validation. You cannot trust AI more than humans...

Let's remove the "examples" set in the previous section. The LLM will be blind once again. As such, he will probably make mistakes but the ToolError exception will guide it onto the correct path. Let's see:


Complete code


from yacana import OllamaAgent, Tool, Task, ToolError

agent1 = OllamaAgent("Ai assistant", "llama3.1:8b")

def adder(first_number: int, second_number: int) -> int:
    print(f"Tool adder was called with param '{first_number}' ({type(first_number)}) and '{second_number}' ({type(second_number)})")
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    ret = first_number + second_number
    print("Result of added tool is: ", ret)
    return ret

adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder)

result: str = Task(f"What's 2+2 ?", agent1, tools=[adder_tool]).solve().content

print(f"Equation result = {result}")
					

▶️ Output:


INFO: [PROMPT][To: Ai assistant]: I give you the following tool definition that you must use to fulfill a future task: adder(first_number: int, second_number: int) -> int - Adds two numbers and returns the result. Please acknowledge the given tool.

INFO: [AI_RESPONSE][From: Ai assistant]: Acknowledged!

I have taken note of the `adder` tool definition:

`adder(first_number: int, second_number: int) -> int`

This tool takes two integer arguments, `first_number` and `second_number`, and returns their sum as an integer.

I'm ready to use this tool when needed.

INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect argument type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: Thank you for the clarification.

For the `adder` tool, I will extract each parameter and use it as a JSON key. Here is the result:

{"first__number": "arg 0", "second__number": "arg 1"}

I will make sure to respect the argument types (in this case, both are integers) when using this structure in the future.

Thank you for the guidance!

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. Use the tool at your disposition to solve the task by outputting as JSON the correct arguments. In return, you will get an answer from the tool. The task is:
What's 2+2 ?

INFO: [AI_RESPONSE][From: Ai assistant]: {"first__number": 2, "second__number": 2}

WARNING: Yacana failed to call tool 'Adder' correctly based on the LLM output


INFO: [PROMPT][To: Ai assistant]: The tool returned an error: `adder() got an unexpected keyword argument 'first__number'`
Using this error message, fix the JSON arguments you gave.
Remember that you must output ONLY the tool arguments as valid JSON. For instance: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": "arg 0", "second_number": "arg 1"}
Tool adder was called with param 'arg 0' (<class 'str'>) and 'arg 1' (<class 'str'>)

WARNING: Tool 'Adder' raised an error


INFO: [PROMPT][To: Ai assistant]: The tool returned an error: `Parameter 'first_number' expected a type integer`
Using this error message, fix the JSON arguments you gave.


INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 2, "second_number": 2}
Tool adder was called with param '2' (<class 'int'>) and '2' (<class 'int'>)
Result of added tool is:  4
Equation result = 4
					

It worked!

2 warnings happened here:

  • "WARNING: Yacana failed to call tool 'Adder' correctly based on the LLM output"
  • "WARNING: Tool 'Adder' raised an error"
  • Warning 1: Regarding the first one if you look closely at the output you can see a strange malformation in the JSON: {"first__number": "arg 0", "second__number": "arg 1"}. The first parameter was called with two underscores for some reason (LLMs...). Fortunately, Yacana banged on the LLM's head and it was fixed in the next iteration.
  • Warning 2: Concerning the second warning, it was the tool itself that raised the exception: The tool returned an error: Parameter 'first_number' expected a type integer. This is only logical as the LLM sent catastrophic values to the tool: {'first_number': 'arg 0', 'second_number': 'arg 1'}. When the ToolError was raised the error message was given to the LLM and a third iteration started. This time all was correct: {"first_number": 2, "second_number": 2} and we got our result from the tool which is 4.

Providing tool call examples

If you followed this tutorial from the start, you saw that multi-shot prompting can sometime help getting more accurate outputs. The Tool class allows this too, using the usage_examples=[] optional parameter. You can provide a Python dictionary where each key corresponds to a function's parameter and the value, a valid value. It's inside an array so you can provide multiple examples if needed. In general one or two is enough.
These dictionaries will be presented by Yacana to the LLM as examples of how to call the tool correctly.

Let's look at an example with this new tool instance:


adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder, usage_examples=[{"first_number": 2, "second_number": 4}, {"first_number": 8, "second_number": -2}])
					

We provided above two examples for the LLM to look at. Each time giving first_number and second_number different integer values. No strings. Actual integers!

Let's run our program again and see if we get the correct input types this time:


INFO: [PROMPT][To: Ai assistant]: I give you the following tool definition that you must use to fulfill a future task: adder(first_number: int, second_number: int) -> int - Adds two numbers and returns the result. Please acknowledge the given tool.

INFO: [AI_RESPONSE][From: Ai assistant]: Acknowledged!

The tool definition provided is:

`adder(first_number: int, second_number: int) -> int`

This tool takes two integer inputs `first_number` and `second_number`, and returns their sum as an integer.

I'm ready to use this tool for any future tasks that require addition!

INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect the arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: I understand now!

So, for the `adder` tool, I need to extract each parameter and structure it as a JSON key-value pair. Here's the result:

{'first_number': 'int', 'second_number': 'int'}

This means that when using this tool, I should specify two integer values for `first_number` and `second_number`, respectively.

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. Use the tool at your disposition to solve the task by outputting as JSON the correct arguments. In return, you will get an answer from the tool. The task is:
What's 2+2 ?

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 2, "second_number": 2}

Tool adder was called with param '2' (<class 'int'>) and '2' (<class 'int'>)
Equation result =  4
				

It worked!
The LLM saw that the tool needed integers for input. As such, it called the tool with the correct types therefore the adder tool returned 4 as it was expected. Hurray!

⚠️ Do not abuse this technic as it tends to create some noise. Trying to manage too many hypothetical use cases might, in the end, degrade the performance of the tool call.

You should combine both server-side validation and multi-shot prompting.
Providing one example may prevent one tool call failure hence less lost CPU time. Though, adding many validation checks in your tool raising with explicit error messages is the best way to ensure that nothing breaks! Nothing beats good all fashion if checks!

Maximum tool errors

What happens if the LLM is stubborn and gets stuck in a loop? Even though Yacana's percussive maintenance should avoid that by shifting LLM internal configuration during runtime more or less randomly, the LLM still might go into an infinite loop. And this is NOT a viable option!
Fortunately, Yacana comes with a default of 5 iterations (tries) for each of the 2 types of errors we encountered earlier:

  • Either the calling error like the "first__number" error seen above.
  • Or the custom ToolError that the tool threw. This means that if one of these two counters gets to 5 then an error is raised. One that is not caught by Yacana.
    Specifically a MaxToolErrorIter exception. You should try/catch all of your Tasks that utilize Tools as they might loop too many times and trigger this exception.

However, you can also set these counters to the value you wish... Move them higher or lower with the following Tool optional parameters: max_call_error: int = 5, max_custom_error: int = 5 For instance:


# Doubling the number of iterations the LLM can do before raising `MaxToolErrorIter`: 5 -> 10
adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder, max_custom_error=10, max_call_error=10)
					


Choosing tool calling style

Tool calling refers to the ability to call your application's functions. So far you used the default tool calling style wich is Yacana style.
But like everything on this earth it has its pros and cons. Also, you can change it to the one from OpenAi.

Difference between Yacana and OpenAi tool calling styles

Both can call functions inside your application but have pros and cons.

▶️ A tool should use OpenAi style when:

  • The LLM has been fine-tuned on tool calling.
  • Tool calling needs to happen fast.
  • LLM is smart (like chatGPT or opensource 45B+).
You can find if your LLM supports tool calling in the model's card description. In ollama you can even filter LLMs by tool calling support.

▶️ A tool should use the Yacana style when:
  • The LLM isn't fine-tuned on tool calling.
  • Time is not the issue.
  • LLM knows english.
  • LLM is dumb (8B-).
  • Your Task has multiple assigned tools and needs native multi turn tool calls.

Changing tool execution type

To change the tool type to OpenAi use the tool_type= parameter in the Tool's constructor.
Available values are ToolType.YACANA or ToolType.OPENAI.
Let's see an example:

from yacana import OllamaAgent, Tool, Task, ToolError, ToolType

agent1 = OllamaAgent("Ai assistant", "llama3.1:8b")

def adder(first_number: int, second_number: int) -> int:
    print(f"Tool adder was called with param '{first_number}' ({type(first_number)}) and '{second_number}' ({type(second_number)})")
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    ret = first_number + second_number
    print("Result of added tool is: ", ret)
    return ret

# => Here we set the tool type to OPENAI
adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder, tool_type=ToolType.OPENAI)

result: str = Task(f"What's 2+2 ?", agent1, tools=[adder_tool]).solve().content

print(f"Equation result = {result}")                    
                

Output:

INFO: [PROMPT][To: Ai assistant]: What's 2+2 ?

WARNING: You chose to use the OpenAI style tool calling with the OllamaAgent for the tool 'Adder'. This tool is set by default as optional=False (hence making it mandatory to use). Note that Ollama does NOT support setting tools optional status on tools! They are all optional by default and this cannot be changed. Yacana may in the future mitigate this issue. If this is important for you please open an issue on the Yacana Github. You can hide this warning by setting `shush=True` in the Tool constructor.

INFO: [AI_RESPONSE][From: Ai assistant]: [{"id": "756f8bd1-0df9-471a-8463-dcc9ed36d68b", "type": "function", "function": {"name": "Adder", "arguments": {"first_number": 2, "second_number": 2}}}]
Tool adder was called with param '2' (<class 'int'>) and '2' (<class 'int'>)
Result of added tool is:  4

INFO: [TOOL_RESPONSE][Adder]: 4


INFO: [PROMPT][To: Ai assistant]: Retrying with original task and tools answer: 'What's 2+2 ?'

INFO: [AI_RESPONSE][From: Ai assistant]: Simple math problem! The answer is indeed 4.
Equation result = Simple math problem! The answer is indeed 4.                    
                

As you can see above, the LLM outputed a function calling JSON string: [{"id": "XX", "type": "function", "function": {"name": "Adder", "arguments": {"first_number": 2, "second_number": 2}}}].
This method is way faster than the Yacana style. But as mentioned before, not all LLMs support this type of tool calling.

Note that you can change the tool_type member of a Tool instance during runtime and not only in the constructor.
Meaning that you can use the same tool with different execution types in 2 separate Tasks: tool_instance.tool_type=ToolType.OPENAI.

Optional tools

Sometimes you assign a Tool to a Task without knowing for sure that the tool will be useful. If you have a fine-tuned model or doing basic operations you may want to rely on the LLM's reasoning to choose if it really needs to call the tool or use his own training knowledge. Setting the optional: bool = True will tweak how Yacana proposes the Tools to the LLM, leaving it a chance to pass on the offer of the tool and use its own knowledge instead.


Forcing tool call and giving it a prompt that makes sense


To demonstrate this, let's make a tool that returns a temperature from a city. It will return a fake temperature as we don't really care.
We won't set optional=True so it will be forced to use the tool:


from yacana import Task, OllamaAgent, Tool

def get_temperature(city: str) -> int:
    return 20

agent1 = OllamaAgent("Ai assistant", "llama3.1:8b")

result: str = Task(f"What's the temperature in NY ?", agent1, tools=[Tool("get_temp", "Returns the celsius temperature of a given city", get_temperature)]).solve().content

print(f"Temperature = {result}")						
					

▶️ Output:


INFO: [PROMPT][To: Ai assistant]: I give you the following tool definition that you must use to fulfill a future task: get_temperature(city: str) -> int - Adds two numbers and returns the result. Please acknowledge the given tool.

INFO: [AI_RESPONSE][From: Ai assistant]: I've received the tool definition!

So, I understand that I have been given a tool called `get_temperature` that takes one argument `city` which is a string, and it's supposed to return an integer.

However, I notice that the tool definition says "Adds two numbers and returns the result" but the function name `get_temperature` suggests it should be related to getting the temperature of a city.

I'll make sure to keep this in mind for any future tasks that require using this tool!

INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'city': 'arg 0'}

INFO: [AI_RESPONSE][From: Ai assistant]: So I understand now. Thank you for the clarification!

To use the `get_temperature` tool, I will extract each parameter and use it as a JSON key with its corresponding value.

The tool definition is: `get_temperature(city: str) -> int`

So, I will structure my tool call like this:  {"city": "arg0"}

Please let me know what's the next step!

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. Use the tool at your disposition to solve the task by outputting as JSON the correct arguments. In return you will get an answer from the tool. The task is:
What's the temperature in NY ?

INFO: [AI_RESPONSE][From: Ai assistant]: { "city": "NY" }
Temperature = 20			
					

The tool was called with { "city": "NY" } which returned 20. Good!



Forcing tool call and giving it a prompt that DOESN'T make sense


Now let's ask something completely unrelated but keep the tool mandatory:


result: str = Task(f"Why is the sky blue ?", agent1, tools=[Tool("get_temp", "Returns the celsius temperature of a given city", get_temperature)]).solve().content					
					

▶️ Output:


INFO: [PROMPT][To: Ai assistant]: I give you the following tool definition that you must use to fulfill a future task: get_temperature(city: str) -> int - Returns the celsius temperature of a given city. Please acknowledge the given tool.

INFO: [AI_RESPONSE][From: Ai assistant]: Acknowledged!

You've provided me with a tool definition for `get_temperature(city: str) -> int`, which returns the Celsius temperature of a given city.

I'm ready to use this tool whenever I need it in future tasks!

INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect the arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'city': 'arg 0'}

INFO: [AI_RESPONSE][From: Ai assistant]: For the `get_temperature( city: str ) -> int` tool, I understand that I need to extract each parameter and use it as a JSON key.

So, for this specific tool, I would structure my JSON call as follows:

{"city": "arg0"}

Where "arg0" is replaced with the actual value of the city I'm interested in.

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. Use the tool at your disposition to solve the task by outputting as JSON the correct arguments. In return you will get an answer from the tool. The task is:
Why is the sky blue ?

INFO: [AI_RESPONSE][From: Ai assistant]: {"city": "Paris"}
Temperature = 20
					

It asked the tool for the temperature in the city 'Paris' without any particular reason. We can't blame it. The tool we provided and the Task to solve have nothing to do with each other. So it's making things up, like this city name.


Optional tool call and giving it a prompt that DOESN'T make sense


Now let's make the tool optional with optional=True and keep our unrelated Task:


result: str = Task(f"Why is the sky blue ?", agent1, tools=[Tool("get_temp", "Returns the celsius temperature of a given city", get_temperature, optional=True)]).solve().content						
					

▶️ Output:


INFO: [PROMPT][To: Ai assistant]: I give you the following tool definition that you may use to fulfill a future task: get_temperature(city: str) -> int - Returns the celsius temperature of a given city. Please acknowledge the given tool.

INFO: [AI_RESPONSE][From: Ai assistant]: Acknowledged!

I have taken note of the `get_temperature` tool, which takes a city name as input (`city: str`) and returns the Celsius temperature of that city (`-> int`). I'm ready to use this tool whenever you'd like me to fulfill a task in the future!

INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'city': 'arg 0'}

INFO: [AI_RESPONSE][From: Ai assistant]: So for the `get_temperature( city: str )` tool, I would extract the single parameter as:

{"city": "arg0"}

Please let me know what's next!

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. In your opinion, is using the tool "get_temp" relevant to solve the task or not ? The task is:
Why is the sky blue ?

INFO: [AI_RESPONSE][From: Ai assistant]: Ha ha, that's an interesting question!

In my opinion, using the `get_temperature` tool is NOT relevant to solving this task. The task asks about why the sky is blue, and temperature doesn't seem to be directly related to that.

So, I'll have to come up with a different approach or use a different tool (if one exists) to tackle this question!

INFO: [PROMPT][To: Ai assistant]: To summarize in one word your previous answer. Do you wish to use the tool or not ? Respond ONLY by 'yes' or 'no'.

INFO: [AI_RESPONSE][From: Ai assistant]: no

INFO: [PROMPT][To: Ai assistant]: Why is the sky blue ?

INFO: [AI_RESPONSE][From: Ai assistant]: A question that has puzzled humans for centuries!

The sky appears blue because of a phenomenon called Rayleigh scattering, named after the British physicist Lord Rayleigh. Here's what happens:

1. **Sunlight**: The sun emits white light, which is made up of all the colors of the visible spectrum (red, orange, yellow, green, blue, indigo, and violet).
2. **Atmosphere**: When this sunlight enters Earth's atmosphere, it encounters tiny molecules of gases like nitrogen (N2) and oxygen (O2). These molecules are much smaller than the wavelength of light.
[BLABLA]
5. **Sky appears blue**: The combined effect of all these scattered blue and violet photons is what makes the sky appear blue to our eyes! The more direct sunlight that reaches our eyes, the whiter it will appear.

So, to summarize: the sky appears blue because of the selective scattering of shorter wavelengths (like blue and violet) by tiny molecules in the atmosphere, which dominates the colors we see when looking up at the sky.
                

As you can see it chose to ignore the tool when Yacana proposed it. It said:


						In my opinion, using the `get_temperature` tool is NOT relevant to solving this task. The task asks about why the sky is blue, and temperature doesn't seem to be directly related to that.
					

Ollama and optional tool support


When using the OllamaAgent with the tool type ToolType.OPENAI you may encounter a minor issue.
Ollama doesn't support mandatory tools when using the OpenAi tool calling style. So, all Tools are optional by default and setting optional=False in the Tool's constructor will have no effect.
It you realy need to enforce a Tool as mandatory then set the tool's type as ToolType.YACANA (default).


Assigning multiple Tools

In this section, we will see that you can assign more than one tool to a Task. You can add as many Tools as you wish and the LLM will be asked what tool it wants to use. After using one of the tools it will be asked if it considers its Task complete. If it says "no" then Yacana will propose the list of tools again and a new iteration starts.

This is roughly what the tool-calling mechanism looks like:
toolcall1B

This doesn't take into account many tweaks Yacana makes like model's runtime config updates (in case of infinite loops), optional tools, self-reflection, multi-shot tool call examples, history cleaning, exiting when reaching max iterations, etc. However, it's a good representation of the internal process of calling tools one after the other.

Additional behavior information:

When only one tool is assigned, the Agent won't be proposed to use it again. One tool is one shot! When giving multiple tools, the agent will then be proposed to use another tool. He could choose to always use the same one though.
In the future, Yacana may allow you to have more control over how the tools are being chosen.

⚠️ When assigning multiple tools to a Task, all Tools are considered optional! Setting the optional=True/False in the Task's constructor will not have any effects anymore!


Let's make a more advanced calculator and solve 2 + 2 - 6 * 8. We'll add the missing tools and give them some "server-side" checking to help the LLM use them properly.


from yacana import Task, OllamaAgent, Tool, ToolError

def adder(first_number: int, second_number: int) -> int:
    print("Adder was called with types = ", str(type(first_number)), str(type(second_number)))
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    print(f"Adder was called with param = |{first_number}| and |{second_number}|")
    return first_number + second_number

def multiplier(first_number: int, second_number: int) -> int:
    print("Multiplier was called with types = ", str(type(first_number)), str(type(second_number)))
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    print(f"Multiplier was called with param = |{first_number}| and |{second_number}|")
    return first_number * second_number

def substractor(first_number: int, second_number: int) -> int:
    print("substractor was called with types = ", str(type(first_number)), str(type(second_number)))
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    print(f"substractor was called with param = |{first_number}| and |{second_number}|")
    return first_number - second_number



agent1 = OllamaAgent("Ai assistant", "llama3.1:8b")

# Defining 3 tools
adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder)
substractor_tool: Tool = Tool("Substractor", "Subtracts two numbers and returns the result. When having, for instance, 2 - 6 the arguments are 2 and 6 and not 2 and -6. The tool does the subtraction.", substractor, usage_examples=[{"first_number": 2, "second_number": 4}])
multiplier_tool: Tool = Tool("Multiplier", "Multiplies two numbers and returns the result.", multiplier)

# Creating a Task to solve and assigning tools to it
result: str = Task(
    f"What's the result of '2 + 2 - 6 * 8'. Decompose the math before trying to solve it. Start with the multiplication. Use the tools at your disposal and do not do the maths yourself.",
    agent1, tools=[
        adder_tool,
        substractor_tool,
        multiplier_tool
    ]).solve().content


print("Result = ", result)
					

▶️ Output:


INFO: [PROMPT][To: Ai assistant]: You have access to this list of tools definitions you can use to fulfill tasks :
* Name: 'Adder' - Usage: adder(first_number: int, second_number: int) -> int - Description: Adds two numbers and returns the result
* Name: 'Substractor' - Usage: substractor(first_number, second_number) -> int - Description: Subtracts two numbers and returns the result. When having, for instance, 2 - 6 the arguments are 2 and 6 and not 2 and -6. The tool does the subtraction.
* Name: 'Multiplier' - Usage: multiplier(first_number, second_number) -> int - Description: Multiplies two numbers and returns the result.
Please acknowledge the given tools.

INFO: [AI_RESPONSE][From: Ai assistant]: I have access to the following tools:

1. **Adder**: Adds two numbers together and returns the result.
        * Usage: `adder(first_number: int, second_number: int) -> int`
2. **Substractor**: Subtracts one number from another (regardless of order) and returns the result.
        * Usage: `substractor(first_number, second_number) -> int`
3. **Multiplier**: Multiplies two numbers together and returns the result.
        * Usage: `multiplier(first_number, second_number) -> int`

I'm ready to use these tools to perform mathematical operations!

INFO: [PROMPT][To: Ai assistant]: You have a task to solve. I will give it to you between these tags `<task></task>`. However, your actual job is to decide if you need to use any of the available tools to solve the task or not. If you do need tools then output their names. The task to solve is <task>What's the result of '2 + 2 - 6 * 8'. Decompose the math before trying to solve it. Start with the multiplication. Use the tools at your disposal and do not do the maths yourself.</task> So, would any tools be useful in relation to the given task ?

INFO: [AI_RESPONSE][From: Ai assistant]: To decompose the math expression '2 + 2 - 6 * 8', I need to follow the order of operations (PEMDAS/BODMAS). According to this rule, multiplication should be done before addition and subtraction.

So, the first step is to calculate the result of `6 * 8`. This requires using the **Multiplier** tool.

INFO: [PROMPT][To: Ai assistant]: In order to summarize your previous answer in one word. Did you chose to use any tools ? Respond ONLY by 'yes' or 'no'.

INFO: [AI_RESPONSE][From: Ai assistant]: Yes.

INFO: [PROMPT][To: Ai assistant]: You can only use one tool at a time. From this list of tools which one do you want to use: [Adder, Substractor, Multiplier]. You must answer ONLY with the single tool name. Nothing else.

INFO: [AI_RESPONSE][From: Ai assistant]: Yes
Multiplier

WARNING: Tool choice was not an exact match but a substring match


INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: {'first_number': '6', 'second_number': '8'}

INFO: [PROMPT][To: Ai assistant]: Now that I showed you examples on how the tool is used it's your turn. Output the tool as valid JSON.

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": "6", "second_number": "8"}
Multiplier was called with types =  <class 'str'> <class 'str'>

WARNING: Tool 'Multiplier' raised an error


INFO: [PROMPT][To: Ai assistant]: The tool returned an error: `Parameter 'first_number' expected a type integer`
Using this error message, fix the JSON arguments you gave.


INFO: [AI_RESPONSE][From: Ai assistant]: {"arg1": 6, "arg2": 8}

WARNING: Yacana failed to call tool 'Multiplier' correctly based on the LLM output


INFO: [PROMPT][To: Ai assistant]: The tool returned an error: `multiplier() got an unexpected keyword argument 'arg1'`
Using this error message, fix the JSON arguments you gave.
Remember that you must output ONLY the tool arguments as valid JSON. For instance: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 6, "second_number": 8}
Multiplier was called with types =  <class 'int'> <class 'int'>
Multiplier was called with param = |6| and |8|

INFO: [TOOL_RESPONSE][Multiplier]: 48


INFO: [PROMPT][To: Ai assistant]: Now that the tool responded do you need to make another tool call ? Explain why and what are the remaining steps are if any.

INFO: [AI_RESPONSE][From: Ai assistant]: The result of `6 * 8` is 48.

Next, I need to calculate the result of `2 + 2 - 48`. To do this, I need to use the **Adder** tool to add `2 + 2`, and then use the **Substractor** tool to subtract the result from the multiplication (`48`) from the sum.

INFO: [PROMPT][To: Ai assistant]: To summarize your previous answer in one word. Do you need to make another tool call ? Answer ONLY by 'yes' or 'no'.

INFO: [AI_RESPONSE][From: Ai assistant]: Yes

INFO: Continuing tool calls loop


INFO: [PROMPT][To: Ai assistant]: You can only use one tool at a time. From this list of tools which one do you want to use: [Adder, Substractor, Multiplier]. You must answer ONLY with the single tool name. Nothing else.

INFO: [AI_RESPONSE][From: Ai assistant]: Yes
Adder

WARNING: Tool choice was not an exact match but a substring match


INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 2, "second_number": 2}

INFO: [PROMPT][To: Ai assistant]: Now that I showed you examples on how the tool is used it's your turn. Output the tool as valid JSON.

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 48, "second_number": -46}
Adder was called with types =  <class 'int'> <class 'int'>
Adder was called with param = |48| and |-46|

INFO: [TOOL_RESPONSE][Adder]: 2


INFO: [PROMPT][To: Ai assistant]: Now that the tool responded do you need to make another tool call ? Explain why and what are the remaining steps are if any.

INFO: [AI_RESPONSE][From: Ai assistant]: The result of `2 + 2` is indeed 4.

Next, I still need to subtract 48 from this result (4) using the **Substractor** tool.

So, yes, I need another tool call. The remaining step is to use the **Substractor** tool with arguments `(4, 48)`.

INFO: [PROMPT][To: Ai assistant]: To summarize your previous answer in one word. Do you need to make another tool call ? Answer ONLY by 'yes' or 'no'.

INFO: [AI_RESPONSE][From: Ai assistant]: Yes
Substractor

INFO: Continuing tool calls loop


INFO: [PROMPT][To: Ai assistant]: You can only use one tool at a time. From this list of tools which one do you want to use: [Adder, Substractor, Multiplier]. You must answer ONLY with the single tool name. Nothing else.

INFO: [AI_RESPONSE][From: Ai assistant]: Yes
Substractor

WARNING: Tool choice was not an exact match but a substring match


INFO: [PROMPT][To: Ai assistant]: To use the tool you MUST extract each parameter and use it as a JSON key like this: {"arg1": "<value1>", "arg2": "<value2>"}. You must respect arguments type. For instance, the tool `getWeather(city: str, lat: int, long: int)` would be structured like this {"city": "new-york", "lat": 10, "lon": 20}. In our case, the tool call you must use must look like that: {'first_number': 'arg 0', 'second_number': 'arg 1'}

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 4, "second_number": 48}

INFO: [PROMPT][To: Ai assistant]: Now that I showed you examples on how the tool is used it's your turn. Output the tool as valid JSON.

INFO: [AI_RESPONSE][From: Ai assistant]: {"first_number": 4, "second_number": 48}
substractor was called with types =  <class 'int'> <class 'int'>
substractor was called with param = |4| and |48|

INFO: [TOOL_RESPONSE][Substractor]: -44


INFO: [PROMPT][To: Ai assistant]: Now that the tool responded do you need to make another tool call ? Explain why and what are the remaining steps are if any.

INFO: [AI_RESPONSE][From: Ai assistant]: The result of `2 + 2 - 6 * 8` is indeed `-44`.

Since I have now calculated all parts of the original expression, there are no more remaining steps or tool calls needed. The task is complete!

INFO: [PROMPT][To: Ai assistant]: To summarize your previous answer in one word. Do you need to make another tool call ? Answer ONLY by 'yes' or 'no'.

INFO: [AI_RESPONSE][From: Ai assistant]: no

INFO: Exiting tool calls loop

Result =  The result of `2 + 2 - 6 * 8` is indeed `-44`.

Since I have now calculated all parts of the original expression, there are no more remaining steps or tool calls needed. The task is complete!

					

-44 is the correct answer!


Multiple tools with OpenAi tool execution

In the previous snippet we did not set the ToolType so the default was Yacana.
Now, let's use the OpenAi tool calling style which is faster to execute!
But before we do, there is a key difference to understand about how both mode behave.

▶️ Yacana relies on iterative prompt engineering to make the tool calls.
This means that when a tool returns a value, Yacana will prompt the LLM to see if it wants to use another one. That's why you can call 3 tools with only one Task.

It looks like that:
Tool calling with multiple tools using yacana.

▶️ On the other hand, if using the OpenAi tool execution a Task can only call ONE tool.

It looks like that:
Tool calling with multiple tools using OpenAi can only call one tool at a time. If you need the LLM to call more than one Tool you will have to do the prompt engineering yourself...


Let's fix that by implementing the logic ourselves!

We'll make a loop allowing the LLM to decide if it must keep calling tools or not :
Tool calling with multiple tools using OpenAi can only call one tool at a time. In the above example the LLM called the 3 tools one after the other using results it got from previous iterations.


from yacana import Task, OllamaAgent, Tool, ToolError, ToolType

def adder(first_number: int, second_number: int) -> int:
    print("Adder was called with types = ", str(type(first_number)), str(type(second_number)))
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    print(f"Adder was called with param = |{first_number}| and |{second_number}|")
    return first_number + second_number

def multiplier(first_number: int, second_number: int) -> int:
    print("Multiplier was called with types = ", str(type(first_number)), str(type(second_number)))
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    print(f"Multiplier was called with param = |{first_number}| and |{second_number}|")
    return first_number * second_number

def substractor(first_number: int, second_number: int) -> int:
    print("substractor was called with types = ", str(type(first_number)), str(type(second_number)))
    if not (isinstance(first_number, int)):
        raise ToolError("Parameter 'first_number' expected a type integer")
    if not (isinstance(second_number, int)):
        raise ToolError("Parameter 'second_number' expected a type integer")
    print(f"substractor was called with param = |{first_number}| and |{second_number}|")
    return first_number - second_number

# Defining 3 tools as optional and ToolType set to OPENAI
adder_tool: Tool = Tool("Adder", "Adds two numbers and returns the result", adder, optional=True, tool_type=ToolType.OPENAI)
substractor_tool: Tool = Tool("Substractor", "Subtracts two numbers and returns the result. When having, for instance, 2 - 6 the arguments are 2 and 6 and not 2 and -6. The tool does the subtraction.", substractor, optional=True, tool_type=ToolType.OPENAI)
multiplier_tool: Tool = Tool("Multiplier", "Multiplies two numbers and returns the result.", multiplier, optional=True, tool_type=ToolType.OPENAI)

agent1 = OllamaAgent("Ai assistant", "llama3.1:8b")

# Initial Task
Task(f"What's `2 + 2 - 6 * 8` ? You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correcttly the equation. You are not allowed to guess the result of any operation. Wait for each tool result before continuing. Only call one tool at a time. When you belive you're finished output 'FINISH'.", agent1, tools=[adder_tool, substractor_tool, multiplier_tool]).solve()

# Creating Tasks in a loop until the LLM decides it's done
while True:
    msg = Task(f"Continue solving the equation! You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correctly the equation. Only call one tool at a time. When you belive you're finished output 'FINISH'", agent1, tools=[adder_tool, substractor_tool, multiplier_tool]).solve()
    
    # If the LLM prints "finish" then it means it's done and we exit the loop
    if "finish" in msg.content.lower():
        break
                

Output:

INFO: [PROMPT][To: Ai assistant]: What's `2 + 2 - 6 * 8` ? You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correcttly the equation. You are not allowed to guess the result of any operation. Wait for each tool result before continuing. Only call one tool at a time. When you belive you're finished output 'FINISH'.

INFO: [AI_RESPONSE][From: Ai assistant]: [{"id": "28f9264d-8064-46e6-b59e-01fe0d6147e5", "type": "function", "function": {"name": "Multiplication", "arguments": {"number_one": 6, "number_two": 8}}}]
tool = 6 * 8 =  48

INFO: [TOOL_RESPONSE][Multiplication]: 48


INFO: [PROMPT][To: Ai assistant]: Retrying with original task and tools answer: 'What's `2 + 2 - 6 * 8` ? You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correcttly the equation. You are not allowed to guess the result of any operation. Wait for each tool result before continuing. Only call one tool at a time. When you belive you're finished output 'FINISH'.'

INFO: [AI_RESPONSE][From: Ai assistant]: Now that we have the result of the multiplication, let's go back to our original equation:

`2 + 2 - 48`

Next operation is addition.

{"name": "Addition", "parameters": {"number_one":2,"number_two":2}}

INFO: [PROMPT][To: Ai assistant]: Continue solving the equation! You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correctly the equation. Only call one tool at a time. When you belive you're finished output 'FINISH'

INFO: [AI_RESPONSE][From: Ai assistant]: [{"id": "3741caad-0030-401e-a92f-ffe29889c2da", "type": "function", "function": {"name": "Addition", "arguments": {"number_one": 2, "number_two": 2}}}]
tool = 2 + 2 =  4

INFO: [TOOL_RESPONSE][Addition]: 4


INFO: [PROMPT][To: Ai assistant]: Retrying with original task and tools answer: 'Continue solving the equation! You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correctly the equation. Only call one tool at a time. When you belive you're finished output 'FINISH''

INFO: [AI_RESPONSE][From: Ai assistant]: Now that we have the result of the addition, let's go back to our original equation:

`4 - 48`

Next operation is subtraction.

{"name": "Subtraction", "parameters": {"number_one":4,"number_two":48}}

INFO: [PROMPT][To: Ai assistant]: Continue solving the equation! You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correctly the equation. Only call one tool at a time. When you belive you're finished output 'FINISH'

INFO: [AI_RESPONSE][From: Ai assistant]: [{"id": "f8946e10-d4b1-400d-9836-3932c8d312cc", "type": "function", "function": {"name": "Subtraction", "arguments": {"number_one": 4, "number_two": 48}}}]
tool = 4 - 48 =  -44

INFO: [TOOL_RESPONSE][Subtraction]: -44


INFO: [PROMPT][To: Ai assistant]: Retrying with original task and tools answer: 'Continue solving the equation! You can only do one operation at a time. Don't worry you will be ask to continue with the operations later. Follow PEMDAS to solve correctly the equation. Only call one tool at a time. When you belive you're finished output 'FINISH''

INFO: [AI_RESPONSE][From: Ai assistant]: Now that we have the result of the subtraction, our equation is complete.

FINISH
                

As you can see, using the OpenAi style executes way faster than the Yacana style. However, it requires you to inplement the looping mechanism yourself.

Pagination