# Tool-Calling Agent

Source: https://staging-docs.aiand.com/cookbook/tool-agent/

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

## The loop

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.

## Full example

```python
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?"))
```

## Notes

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

<Aside type="tip">
  Need stricter parsing of arguments? Use [Structured Outputs](/capabilities/structured-outputs/) on the result side, or define tighter tool schemas with `additionalProperties: false`.
</Aside>
