Native function calling
Now let us wire everything together. The flow has 5 steps: (1) send message with tools, (2) LLM returns tool_calls, (3) add assistant message, (4) execute functions and add tool results, (5) call LLM again for final answer.
import json
messages = [{"role": "user", "content": "Turn off kitchen lights and check Paris weather"}]
# Step 1: Call LLM with tool schemas
response = completion(
model=DEFAULT_MODEL,
messages=messages,
tools=tools, # Pass the JSON schemas
tool_choice="auto" # Let model decide when to use tools
)
response_message = response.choices[0].message
tool_calls = response_message.get("tool_calls", [])
print(f"Model wants to call {len(tool_calls)} tools")Pass the tools list to completion(). The model returns tool_calls instead of text when it wants to use a tool.
When the LLM requests multiple tool calls at once, the tool_call_id links each result to its corresponding request. Without it, the model would not know which result belongs to which function call. Now let us execute the tools.
# Step 2: Add assistant message (with tool_calls)
messages.append(response_message)
# Step 3: Execute each tool and add results
available_functions = {
"get_weather": get_weather,
"toggle_light": toggle_light
}
for tool_call in tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
# Execute the function
result = available_functions[function_name](**function_args)
# Add tool result to messages
messages.append({
"role": "tool",
"name": function_name,
"content": result,
"tool_call_id": tool_call.id # Links result to request
})Execute each tool call, then add results back with role "tool" and the matching tool_call_id.
With the tool results added to messages, we make one final LLM call. The model sees the results and generates a natural language response for the user.
# Step 4: Call LLM again with tool results
final_response = completion(
model=DEFAULT_MODEL,
messages=messages
)
print(final_response.choices[0].message.content)
# "I've turned off the kitchen lights and the weather
# in Paris is 72°F and sunny."The LLM sees the tool results and generates a natural language summary for the user.
Complete function calling flow (5 steps)
def get_weather(location: str):
return f"The weather in {location} is 72°F and sunny."
def toggle_light(room: str, status: bool):
action = "on" if status else "off"
return f"The lights in the {room} have been turned {action}."Simple tool implementations. In production, these would call real APIs or control real devices.
Matching exercise: Match function calling steps to descriptions
Loading practice…
Since the model only sees the schemas you provide, it should only call tools from that list. But as a safeguard, always check if function_name exists in your available_functions dict before calling it. If it does not exist, return an error message as the tool result. The LLM can often recover from this.
Hints: Hints
Loading practice…
Compare this to the ReAct approach: no regex, no custom system prompts for formatting, no fragile text parsing. The model returns structured data that your code can reliably process. This is the industry standard for tool integration.