Tool loop in Python
The smallest complete tool loop: the model can call two functions as many times as it needs, and the loop ends when it answers in text. Run it with pip install openai and python loop.py.
# loop.py
import json
import os
from datetime import date
from openai import OpenAI
client = OpenAI(base_url="https://app.syntic.ai/v1", api_key=os.environ["SYNTIC_API_KEY"])
# --- Your functions --------------------------------------------------------
LIBRARY = {
"978-0141439518": {"title": "Pride and Prejudice", "copies": 3, "on_loan": 3, "due_back": "2026-10-19"},
"978-0007119318": {"title": "The Hobbit", "copies": 2, "on_loan": 1, "due_back": None},
}
def find_book(query: str) -> dict:
hits = [{"isbn": k, "title": v["title"]} for k, v in LIBRARY.items() if query.lower() in v["title"].lower()]
return {"results": hits}
def availability(isbn: str) -> dict:
book = LIBRARY.get(isbn)
if not book:
return {"error": f"no book with ISBN {isbn}"}
free = book["copies"] - book["on_loan"]
return {"available": free, "next_due_back": None if free else book["due_back"]}
FUNCTIONS = {"find_book": find_book, "availability": availability}
# --- How they are described to the model ---------------------------------
TOOLS = [
{"type": "function", "function": {
"name": "find_book", "description": "Search the catalogue by title words. Returns ISBNs.",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}},
{"type": "function", "function": {
"name": "availability", "description": "Copies available now for an ISBN, and when the next one is due back.",
"parameters": {"type": "object", "properties": {"isbn": {"type": "string"}}, "required": ["isbn"]}}},
]
# --- The loop ---------------------------------------------------------------
def run(question: str, max_rounds: int = 8) -> str:
messages = [
{"role": "system", "content": f"You help members of a town library. Today is {date.today()}. Use the tools; never guess availability."},
{"role": "user", "content": question},
]
for _ in range(max_rounds):
reply = client.chat.completions.create(
model="Amara S1.2", stream=False, max_tokens=800, tools=TOOLS, messages=messages,
)
msg = reply.choices[0].message
if not msg.tool_calls:
return msg.content or ""
messages.append(msg.model_dump(exclude_none=True)) # the assistant turn, unchanged
for call in msg.tool_calls:
try:
args = json.loads(call.function.arguments or "{}")
result = FUNCTIONS[call.function.name](**args)
except Exception as err: # bad JSON, unknown tool, wrong args
result = {"error": str(err)}
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
return "Sorry, I couldn't finish that request."
if __name__ == "__main__":
print(run("Can I borrow Pride and Prejudice today? If not, when?"))Expected behaviour: the model calls find_book("Pride and Prejudice"), then availability("978-0141439518"), then answers that all copies are out and the next is due back on 19 October.
The three rules
- Append the assistant message with its
tool_callsexactly as received. - Add one
toolmessage per call, with the matchingtool_call_id. - Send the same
toolslist on every round.
Errors are returned to the model as results so it can recover, and max_rounds stops a runaway loop.
Related
- Tool loop in TypeScript
- Write a tool loop: streaming,
tool_choiceand production advice. - Tool calling
Was this page helpful?