CookbookTool loop in Go

Tool loop in Go

The model can call two functions, find_book and availability, as many times as it needs; the loop ends when it answers in text. It uses plain HTTP, so there is nothing to install beyond the standard library unless noted.

package main
 
import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
)
 
type Call struct {
	ID       string `json:"id"`
	Type     string `json:"type"`
	Function struct {
		Name      string `json:"name"`
		Arguments string `json:"arguments"`
	} `json:"function"`
}
 
type Message struct {
	Role       string `json:"role"`
	Content    any    `json:"content"`
	ToolCalls  []Call `json:"tool_calls,omitempty"`
	ToolCallID string `json:"tool_call_id,omitempty"`
}
 
var tools = json.RawMessage(`[
  {"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"]}}}
]`)
 
type book struct {
	Title          string
	Copies, OnLoan int
	DueBack        string
}
 
var library = map[string]book{
	"978-0141439518": {"Pride and Prejudice", 3, 3, "2026-10-19"},
	"978-0007119318": {"The Hobbit", 2, 1, ""},
}
 
func runTool(name, rawArgs string) any {
	var args map[string]string
	if err := json.Unmarshal([]byte(rawArgs), &args); err != nil {
		return map[string]string{"error": "arguments were not valid JSON"}
	}
	switch name {
	case "find_book":
		var hits []map[string]string
		for isbn, b := range library {
			if strings.Contains(strings.ToLower(b.Title), strings.ToLower(args["query"])) {
				hits = append(hits, map[string]string{"isbn": isbn, "title": b.Title})
			}
		}
		return map[string]any{"results": hits}
	case "availability":
		b, ok := library[args["isbn"]]
		if !ok {
			return map[string]string{"error": "no book with ISBN " + args["isbn"]}
		}
		free := b.Copies - b.OnLoan
		res := map[string]any{"available": free, "next_due_back": nil}
		if free == 0 {
			res["next_due_back"] = b.DueBack
		}
		return res
	}
	return map[string]string{"error": "unknown tool " + name}
}
 
func complete(messages []Message) (Message, error) {
	body, _ := json.Marshal(map[string]any{"model": "Amara S1.2", "stream": false, "max_tokens": 800, "tools": tools, "messages": messages})
	req, _ := http.NewRequest("POST", "https://app.syntic.ai/v1/chat/completions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SYNTIC_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return Message{}, err
	}
	defer resp.Body.Close()
	var out struct {
		Choices []struct{ Message Message } `json:"choices"`
		Error   *struct{ Code, Message string }
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return Message{}, err
	}
	if out.Error != nil {
		return Message{}, fmt.Errorf("%d %s: %s", resp.StatusCode, out.Error.Code, out.Error.Message)
	}
	return out.Choices[0].Message, nil
}
 
func main() {
	messages := []Message{
		{Role: "system", Content: "You help members of a town library. Use the tools; never guess availability."},
		{Role: "user", Content: "Can I borrow Pride and Prejudice today? If not, when?"},
	}
	for round := 0; round < 8; round++ {
		msg, err := complete(messages)
		if err != nil {
			panic(err)
		}
		if len(msg.ToolCalls) == 0 {
			fmt.Println(msg.Content)
			return
		}
		messages = append(messages, msg)
		for _, c := range msg.ToolCalls {
			result, _ := json.Marshal(runTool(c.Function.Name, c.Function.Arguments))
			messages = append(messages, Message{Role: "tool", ToolCallID: c.ID, Content: string(result)})
		}
	}
	fmt.Println("Sorry, I couldn't finish that request.")
}

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 from your functions are returned to the model as results so it can recover, and the round limit stops a runaway loop.

Was this page helpful?