CookbookTool loop in Python

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

  1. Append the assistant message with its tool_calls exactly as received.
  2. Add one tool message per call, with the matching tool_call_id.
  3. Send the same tools list on every round.

Errors are returned to the model as results so it can recover, and max_rounds stops a runaway loop.

Was this page helpful?