Tool loop in C#
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. It targets .NET 8 and uses System.Text.Json.Nodes.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
var library = new Dictionary<string, (string Title, int Copies, int OnLoan, string? DueBack)>
{
["978-0141439518"] = ("Pride and Prejudice", 3, 3, "2026-10-19"),
["978-0007119318"] = ("The Hobbit", 2, 1, null),
};
JsonNode RunTool(string name, string rawArgs)
{
try
{
var args = JsonNode.Parse(rawArgs)!;
switch (name)
{
case "find_book":
var q = args["query"]!.GetValue<string>().ToLowerInvariant();
var hits = new JsonArray();
foreach (var (isbn, b) in library)
if (b.Title.ToLowerInvariant().Contains(q)) hits.Add(new JsonObject { ["isbn"] = isbn, ["title"] = b.Title });
return new JsonObject { ["results"] = hits };
case "availability":
var key = args["isbn"]!.GetValue<string>();
if (!library.TryGetValue(key, out var book)) return new JsonObject { ["error"] = $"no book with ISBN {key}" };
var free = book.Copies - book.OnLoan;
return new JsonObject { ["available"] = free, ["next_due_back"] = free == 0 ? book.DueBack : null };
default:
return new JsonObject { ["error"] = $"unknown tool {name}" };
}
}
catch (Exception e) { return new JsonObject { ["error"] = e.Message }; }
}
var tools = JsonNode.Parse("""
[
{"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"]}}}
]
""")!;
var messages = new JsonArray
{
new JsonObject { ["role"] = "system", ["content"] = "You help members of a town library. Use the tools; never guess availability." },
new JsonObject { ["role"] = "user", ["content"] = "Can I borrow Pride and Prejudice today? If not, when?" },
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("SYNTIC_API_KEY"));
for (var round = 0; round < 8; round++)
{
var body = new JsonObject { ["model"] = "Amara S1.2", ["stream"] = false, ["max_tokens"] = 800, ["tools"] = tools.DeepClone(), ["messages"] = messages.DeepClone() };
var res = await http.PostAsync("https://app.syntic.ai/v1/chat/completions", new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json"));
var json = JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
if (!res.IsSuccessStatusCode) throw new Exception(json["error"]!.ToJsonString());
var msg = json["choices"]![0]!["message"]!;
var calls = msg["tool_calls"]?.AsArray();
if (calls is null || calls.Count == 0) { Console.WriteLine(msg["content"]); return; }
messages.Add(msg.DeepClone());
foreach (var call in calls)
{
var result = RunTool(call!["function"]!["name"]!.GetValue<string>(), call["function"]!["arguments"]?.GetValue<string>() ?? "{}");
messages.Add(new JsonObject { ["role"] = "tool", ["tool_call_id"] = call["id"]!.GetValue<string>(), ["content"] = result.ToJsonString() });
}
}
Console.WriteLine("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?