Created
June 8, 2026 07:45
-
-
Save artydev/345a63af28db34f447e7510f23d2dddf to your computer and use it in GitHub Desktop.
LLM - ToolCallinin C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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