Tool loop in Java
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 the only dependency is Jackson (com.fasterxml.jackson.core:jackson-databind) for JSON.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class ToolLoop {
static final ObjectMapper M = new ObjectMapper();
static final HttpClient HTTP = HttpClient.newHttpClient();
record Book(String title, int copies, int onLoan, String dueBack) {}
static final Map<String, Book> LIBRARY = Map.of(
"978-0141439518", new Book("Pride and Prejudice", 3, 3, "2026-10-19"),
"978-0007119318", new Book("The Hobbit", 2, 1, null));
static ObjectNode runTool(String name, String rawArgs) {
ObjectNode out = M.createObjectNode();
try {
JsonNode args = M.readTree(rawArgs);
switch (name) {
case "find_book" -> {
ArrayNode hits = out.putArray("results");
String q = args.path("query").asText().toLowerCase();
LIBRARY.forEach((isbn, b) -> {
if (b.title().toLowerCase().contains(q)) hits.addObject().put("isbn", isbn).put("title", b.title());
});
}
case "availability" -> {
Book b = LIBRARY.get(args.path("isbn").asText());
if (b == null) return out.put("error", "no book with that ISBN");
int free = b.copies() - b.onLoan();
out.put("available", free);
out.put("next_due_back", free == 0 ? b.dueBack() : null);
}
default -> out.put("error", "unknown tool " + name);
}
} catch (Exception e) {
out.put("error", e.getMessage());
}
return out;
}
public static void main(String[] args) throws Exception {
JsonNode tools = M.readTree("""
[
{"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"]}}}
]
""");
ArrayNode messages = M.createArrayNode();
messages.addObject().put("role", "system").put("content", "You help members of a town library. Use the tools; never guess availability.");
messages.addObject().put("role", "user").put("content", "Can I borrow Pride and Prejudice today? If not, when?");
for (int round = 0; round < 8; round++) {
ObjectNode body = M.createObjectNode().put("model", "Amara S1.2").put("stream", false).put("max_tokens", 800);
body.set("tools", tools);
body.set("messages", messages);
HttpRequest req = HttpRequest.newBuilder(URI.create("https://app.syntic.ai/v1/chat/completions"))
.header("Authorization", "Bearer " + System.getenv("SYNTIC_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body))).build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
JsonNode json = M.readTree(res.body());
if (res.statusCode() >= 300) throw new RuntimeException(json.path("error").toString());
JsonNode msg = json.path("choices").get(0).path("message");
if (!msg.has("tool_calls") || msg.path("tool_calls").isEmpty()) {
System.out.println(msg.path("content").asText());
return;
}
messages.add(msg);
for (JsonNode call : msg.path("tool_calls")) {
ObjectNode result = runTool(call.path("function").path("name").asText(), call.path("function").path("arguments").asText("{}"));
messages.addObject().put("role", "tool").put("tool_call_id", call.path("id").asText()).put("content", M.writeValueAsString(result));
}
}
System.out.println("Sorry, I couldn't finish that request.");
}
}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 from your functions are returned to the model as results so it can recover, and the round limit stops a runaway loop.
Related
Was this page helpful?