Skip to content

Instantly share code, notes, and snippets.

@artydev
Created June 8, 2026 07:45
Show Gist options
  • Select an option

  • Save artydev/345a63af28db34f447e7510f23d2dddf to your computer and use it in GitHub Desktop.

Select an option

Save artydev/345a63af28db34f447e7510f23d2dddf to your computer and use it in GitHub Desktop.
LLM - ToolCallinin C
/*
* TestLfmTools.cs
* LFM2.5-1.2B tool calling test — rewritten with the OpenAI NuGet package.
* Ollama exposes an OpenAI-compatible endpoint, so no manual ChatML or
* regex parsing needed.
*
* Requirements: .NET 8+
* dotnet add package OpenAI --version 2.*
*
* NOTE: Tool calling via the OpenAI-compatible endpoint requires the model
* to have a tool template in its Modelfile. The community upload
* hadad/LFM2.5-1.2B lacks one, so create a local model first:
*
* ollama show hadad/LFM2.5-1.2B:Q4_K_M --modelfile > Modelfile
* # add TEMPLATE block (see README) then:
* ollama create lfm2.5-tools -f Modelfile
*
* Or simply swap the model name below for one that supports tools natively,
* e.g. "qwen2.5:1.5b" or "llama3.2:1b".
*
* Build & run:
* dotnet new console -n LfmTest && cd LfmTest
* dotnet add package OpenAI --version 2.*
* cp ../TestLfmTools.cs Program.cs
* dotnet run
*/
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using OpenAI;
using OpenAI.Chat;
await Program.Main();
// ────────────────────────────────────────────────────────────────────────────
static class Program
{
// Ollama's OpenAI-compatible base URL
const string BaseUrl = "http://localhost:11434/v1";
const string Model = "lfm2.5-tools"; // or "qwen2.5:1.5b", "llama3.2:1b", etc.
// ── OpenAI client pointed at local Ollama ────────────────────────────────
static readonly ChatClient Client = new OpenAIClient(
credential: new ApiKeyCredential("ollama"), // Ollama ignores the key
options: new OpenAIClientOptions { Endpoint = new Uri(BaseUrl) }
).GetChatClient(Model);
// ── tool definitions ─────────────────────────────────────────────────────
static readonly ChatTool[] Tools =
{
ChatTool.CreateFunctionTool(
functionName: "get_weather",
functionDescription: "Get the current weather for a given city.",
functionParameters: BinaryData.FromString("""
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g. 'Paris'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit (default: celsius)"
}
},
"required": ["location"]
}
""")
),
ChatTool.CreateFunctionTool(
functionName: "calculate",
functionDescription:
"Evaluate a math expression. " +
"Use standard arithmetic operators: +, -, *, /. " +
"For percentages write '15% of 280' or '0.15 * 280'. " +
"Do NOT pass natural language like 'fifteen percent of 280'.",
functionParameters: BinaryData.FromString("""
{
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression, e.g. '(3+5)*2' or '15% of 280'"
}
},
"required": ["expression"]
}
""")
),
ChatTool.CreateFunctionTool(
functionName: "search_contacts",
functionDescription: "Search for a person in the contact book by name.",
functionParameters: BinaryData.FromString("""
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Full or partial name to search for"
}
},
"required": ["name"]
}
""")
),
};
// ── fake tool implementations ─────────────────────────────────────────────
static string GetWeather(string location, string unit = "celsius")
{
var db = new Dictionary<string, (int temp, string condition, int humidity)>(StringComparer.OrdinalIgnoreCase)
{
["paris"] = (18, "Partly cloudy", 65),
["london"] = (14, "Rainy", 80),
["tokyo"] = (25, "Sunny", 55),
["new york"] = (22, "Clear", 50),
};
var (temp, condition, humidity) = db.TryGetValue(location, out var v)
? v : (20, "Unknown", 60);
if (unit.Equals("fahrenheit", StringComparison.OrdinalIgnoreCase))
temp = (int)Math.Round(temp * 9.0 / 5 + 32);
return JsonSerializer.Serialize(new { temp, condition, humidity, location, unit });
}
static string Calculate(string expression)
{
try
{
// Normalise "15% of 280" → "(15 / 100) * 280"
var expr = System.Text.RegularExpressions.Regex.Replace(
expression,
@"(\d+(?:\.\d+)?)\s*%\s*of\s*(\d+(?:\.\d+)?)",
m => $"({m.Groups[1].Value} / 100) * {m.Groups[2].Value}",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
// Normalise bare "X%" → "(X/100)"
expr = System.Text.RegularExpressions.Regex.Replace(
expr, @"(\d+(?:\.\d+)?)\s*%", "($1/100)");
if (expr.Any(c => !"0123456789+-*/()., ".Contains(c)))
return JsonSerializer.Serialize(new { error = $"Invalid characters: {expr}" });
double result = Convert.ToDouble(new DataTable().Compute(expr, null));
return JsonSerializer.Serialize(new { expression, result = Math.Round(result, 10) });
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = ex.Message });
}
}
static string SearchContacts(string name)
{
var contacts = new[]
{
new { name = "Alice Martin", email = "alice@example.com", phone = "+33 6 12 34 56 78" },
new { name = "Bob Dupont", email = "bob@example.com", phone = "+33 6 98 76 54 32" },
new { name = "Charlie Durand", email = "charlie@example.com", phone = "+33 6 55 44 33 22" },
};
var matches = contacts
.Where(c => c.name.Contains(name, StringComparison.OrdinalIgnoreCase))
.ToArray();
return JsonSerializer.Serialize(new { query = name, results = matches, count = matches.Length });
}
// ── dispatch ──────────────────────────────────────────────────────────────
static string DispatchTool(string name, string argsJson)
{
// Parse the JSON args the model produced
var args = JsonNode.Parse(argsJson) as JsonObject
?? new JsonObject();
string Get(string key, string fallback = "") =>
args[key]?.GetValue<string>() ?? fallback;
return name switch
{
"get_weather" => GetWeather(Get("location"), Get("unit", "celsius")),
"calculate" => Calculate(Get("expression")),
"search_contacts" => SearchContacts(Get("name")),
_ => JsonSerializer.Serialize(new { error = $"Unknown tool: {name}" }),
};
}
// ── agentic loop ──────────────────────────────────────────────────────────
static async Task<string> Run(string userPrompt)
{
Console.WriteLine($"\n{new string('=', 60)}");
Console.WriteLine($"USER: {userPrompt}");
var options = new ChatCompletionOptions();
foreach (var tool in Tools)
options.Tools.Add(tool);
// Start conversation
var messages = new List<ChatMessage>
{
new SystemChatMessage("You are a helpful assistant."),
new UserChatMessage(userPrompt),
};
for (int iter = 0; iter < 5; iter++)
{
ChatCompletion response = await Client.CompleteChatAsync(messages, options);
switch (response.FinishReason)
{
// ── model wants to call tools ─────────────────────────────────
case ChatFinishReason.ToolCalls:
{
// Add assistant message (with tool call requests) to history
messages.Add(new AssistantChatMessage(response));
// Execute each tool call and collect results
var toolResults = new List<ToolChatMessage>();
foreach (var tc in response.ToolCalls)
{
string argsJson = tc.FunctionArguments.ToString();
Console.WriteLine($" → TOOL CALL: {tc.FunctionName}({argsJson})");
string result = DispatchTool(tc.FunctionName, argsJson);
Console.WriteLine($" ← RESULT: {result}");
toolResults.Add(new ToolChatMessage(tc.Id, result));
}
// Feed all results back in one go
messages.AddRange(toolResults);
break;
}
// ── final answer ──────────────────────────────────────────────
case ChatFinishReason.Stop:
{
string answer = response.Content[0].Text;
Console.WriteLine($"ASSISTANT: {answer}");
return answer;
}
default:
Console.WriteLine($" [unexpected finish reason: {response.FinishReason}]");
return "";
}
}
return "(max iterations reached)";
}
// ── test prompts ──────────────────────────────────────────────────────────
static readonly string[] TestPrompts =
{
"What's the weather like in Paris right now?",
"What is (123 * 456) + 789?",
"Find contact info for Alice.",
"What's the weather in Tokyo in Fahrenheit, and also calculate 15% of 280.",
};
// ── main ──────────────────────────────────────────────────────────────────
public static async Task Main()
{
Console.WriteLine($"Model : {Model}");
Console.WriteLine($"URL : {BaseUrl}");
Console.WriteLine("Ensure Ollama is running: ollama serve");
// Quick connectivity check
try
{
using var http = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromSeconds(3) };
await http.GetAsync("http://localhost:11434");
}
catch
{
Console.WriteLine("\nERROR: Cannot reach Ollama at localhost:11434.");
Console.WriteLine("Start it with: ollama serve");
return;
}
int passed = 0;
foreach (var prompt in TestPrompts)
{
try
{
string answer = await Run(prompt);
if (!string.IsNullOrWhiteSpace(answer)) passed++;
}
catch (Exception ex)
{
Console.WriteLine($" ERROR: {ex.Message}");
}
}
Console.WriteLine($"\n{new string('=', 60)}");
Console.WriteLine($"Done: {passed}/{TestPrompts.Length} prompts completed.");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment