Skip to content

Tool-Calling Agent

Build a loop where the model can call your tools, see the results, and keep going until it has an answer.

  1. Define tools as a JSON-schema list.
  2. Send the messages + tools.
  3. If the model returns tool_calls, execute them locally and append the results.
  4. Repeat until the model returns a normal message.
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...")
def get_weather(city: str) -> str:
return json.dumps({"city": city, "temp_c": 22, "conditions": "sunny"})
def search_web(query: str) -> str:
return json.dumps({"results": [{"title": "Tokyo", "url": "https://en.wikipedia.org/wiki/Tokyo"}]})
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
},
},
]
HANDLERS = {"get_weather": get_weather, "search_web": search_web}
def run(question: str) -> str:
messages = [{"role": "user", "content": question}]
for _ in range(10):
response = client.chat.completions.create(
model="openai/gpt-oss-120b",
messages=messages,
tools=TOOLS,
)
msg = response.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
return msg.content
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = HANDLERS[call.function.name](**args)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
raise RuntimeError("Agent exceeded max iterations")
print(run("What's the weather in Tokyo, and is it famous for anything?"))
  • Cap iterations (10 above) so a misbehaving model can’t loop forever.
  • tool_choice="auto" is the default; force a specific tool with tool_choice={"type": "function", "function": {"name": "..."}}.
  • For parallel tool calls in a single turn, leave parallel_tool_calls: true (the default) — the loop above already handles multiple tool_calls per iteration.