Created
June 14, 2026 09:50
-
-
Save artydev/c9a5daa55be92be6e4ba5e63353780ed to your computer and use it in GitHub Desktop.
Engine Ready (qwen2.5:3b)
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
| ```cs | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Net.Http; | |
| using System.Text; | |
| using System.Text.Json; | |
| using System.Text.Json.Serialization; | |
| using System.Text.RegularExpressions; | |
| using System.Threading.Tasks; | |
| // ───────────────────────────────────────────────────────────── | |
| // AOT SERIALIZATION CONTEXT | |
| // ───────────────────────────────────────────────────────────── | |
| [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] | |
| [JsonSerializable(typeof(OllamaRequest))] | |
| [JsonSerializable(typeof(OllamaResponse))] | |
| [JsonSerializable(typeof(OllamaOptions))] | |
| internal partial class OllamaJsonContext : JsonSerializerContext { } | |
| // ───────────────────────────────────────────────────────────── | |
| // TOOL REGISTRY — add / remove tools here only | |
| // ───────────────────────────────────────────────────────────── | |
| /// <summary>Describes one parameter of a tool for prompt generation.</summary> | |
| public record ToolParam(string Name, string Type, string Description); | |
| /// <summary> | |
| /// Describes a tool that the model can call. | |
| /// <para><see cref="Handler"/> receives the raw XML tag string and returns the result string.</para> | |
| /// </summary> | |
| public record ToolDefinition( | |
| string Name, | |
| string Description, | |
| IReadOnlyList<ToolParam> Params, | |
| Func<string, string> Handler | |
| ); | |
| public static class ToolRegistry | |
| { | |
| public static readonly IReadOnlyList<ToolDefinition> Tools = new List<ToolDefinition> | |
| { | |
| // ── greet_user ──────────────────────────────────────── | |
| new( | |
| Name: "greet_user", | |
| Description: "MUST be called whenever the user introduces themselves or provides their name " + | |
| "(e.g. 'I am John', 'my name is Alice', 'hi I'm Marco', 'call me Sam'). " + | |
| "Extract the name and call this tool immediately — do NOT reply in plain text.", | |
| Params: new[] | |
| { | |
| new ToolParam("name", "string", "The user's first name as stated") | |
| }, | |
| Handler: raw => | |
| { | |
| var m = Regex.Match(raw, @"name=""([^""]+)"""); | |
| return m.Success ? $"Bonjour {m.Groups[1].Value}!" : "Error: 'name' parameter not found."; | |
| } | |
| ), | |
| // ── add_numbers ─────────────────────────────────────── | |
| new( | |
| Name: "add_numbers", | |
| Description: "Add two numbers together and return the result.", | |
| Params: new[] | |
| { | |
| new ToolParam("a", "number", "First operand"), | |
| new ToolParam("b", "number", "Second operand") | |
| }, | |
| Handler: raw => | |
| { | |
| var mA = Regex.Match(raw, @"a=""([^""]+)"""); | |
| var mB = Regex.Match(raw, @"b=""([^""]+)"""); | |
| if (mA.Success && mB.Success | |
| && double.TryParse(mA.Groups[1].Value, System.Globalization.NumberStyles.Any, | |
| System.Globalization.CultureInfo.InvariantCulture, out double a) | |
| && double.TryParse(mB.Groups[1].Value, System.Globalization.NumberStyles.Any, | |
| System.Globalization.CultureInfo.InvariantCulture, out double b)) | |
| return (a + b).ToString(System.Globalization.CultureInfo.InvariantCulture); | |
| return "Error: Could not parse 'a' or 'b' as numbers."; | |
| } | |
| ), | |
| // ── ADD NEW TOOLS HERE ──────────────────────────────── | |
| // new( | |
| // Name: "get_weather", | |
| // Description: "Return current weather for a given city.", | |
| // Params: new[] { new ToolParam("city", "string", "City name") }, | |
| // Handler: raw => { /* your logic */ return "Cloudy, 18°C"; } | |
| // ), | |
| }; | |
| // Fast lookup by name | |
| private static readonly Dictionary<string, ToolDefinition> _index = | |
| Tools.ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase); | |
| public static bool TryGet(string name, out ToolDefinition? tool) => | |
| _index.TryGetValue(name, out tool); | |
| } | |
| // ───────────────────────────────────────────────────────────── | |
| // PROMPT BUILDER — turns the registry into the tool block | |
| // ───────────────────────────────────────────────────────────── | |
| public static class PromptBuilder | |
| { | |
| private const string BasePrompt = """ | |
| You are a precise, helpful assistant with access to a dynamic set of tools. | |
| Your primary job is to have natural, useful conversations. Tools exist only to augment | |
| your responses when genuinely needed — never as a reflex. | |
| ══════════════════════════════════════════ | |
| TOOL CALL FORMAT | |
| ══════════════════════════════════════════ | |
| All tools are called using self-closing XML tags: | |
| <call:TOOL_NAME param1="value1" param2="value2" /> | |
| Rules for values: | |
| • Strings → quote as-is: name="Alice" | |
| • Numbers → raw value, no units: a="12" b="7.5" | |
| • Booleans → lowercase string: enabled="true" | |
| • Lists → comma-separated: items="a,b,c" | |
| ══════════════════════════════════════════ | |
| AVAILABLE TOOLS | |
| ══════════════════════════════════════════ | |
| {TOOLS_BLOCK} | |
| ══════════════════════════════════════════ | |
| DECISION FRAMEWORK | |
| ══════════════════════════════════════════ | |
| STEP 1 — THINK silently before every response: | |
| a) What is the user actually asking for? | |
| b) Does any listed tool directly serve that need? | |
| c) Would the tool's output be the core of my answer? | |
| STEP 2 — Use a tool ONLY when ALL of these are true: | |
| ✔ A listed tool exists that matches the user's intent | |
| ✔ The tool's output is the primary value of the response | |
| ✔ The required parameters can be extracted unambiguously from the input | |
| STEP 3 — Do NOT use a tool when: | |
| ✘ The question is answerable directly from your knowledge | |
| ✘ A tool exists but its output is peripheral to the answer | |
| ✘ Parameters are missing or ambiguous — ask the user to clarify instead (CASE D) | |
| ✘ No listed tool matches — never invent or approximate tool names | |
| ✘ You are uncertain whether a tool applies — default to answering naturally | |
| ══════════════════════════════════════════ | |
| OUTPUT FORMAT — FOUR STRICT CASES | |
| ══════════════════════════════════════════ | |
| CASE A — No tool needed → respond naturally in plain text. No XML. No mention of tools. | |
| User: "Who invented the telephone?" | |
| You: "Alexander Graham Bell is credited with inventing the telephone in 1876." | |
| ────────────────────────────────────────── | |
| CASE B — Tool needed → output ONLY the XML tag. Zero preamble. Zero trailing text. | |
| User: "Add 15 and 27" | |
| You: <call:add_numbers a="15" b="27" /> | |
| User: "Hey, I'm Sophie!" | |
| You: <call:greet_user name="Sophie" /> | |
| User: "hello, I am John" | |
| You: <call:greet_user name="John" /> | |
| User: "my name is Alice" | |
| You: <call:greet_user name="Alice" /> | |
| User: "call me Marco" | |
| You: <call:greet_user name="Marco" /> | |
| ────────────────────────────────────────── | |
| CASE C — After a tool result is returned → incorporate it naturally. Be concise and friendly. | |
| Result: "42" | |
| You: "15 + 27 equals 42." | |
| Result: "Bonjour Sophie!" | |
| You: "Bonjour Sophie! How can I help you today?" | |
| ────────────────────────────────────────── | |
| CASE D — Parameters missing or ambiguous → ask ONE targeted clarifying question. No tool call yet. | |
| User: "Add the numbers" | |
| You: "I'd be happy to add them — which two numbers should I use?" | |
| ══════════════════════════════════════════ | |
| CHAINING TOOLS | |
| ══════════════════════════════════════════ | |
| If a task requires multiple tool calls in sequence: | |
| • Execute one tool call per response turn | |
| • Wait for the result before calling the next tool | |
| • Never batch multiple <call:...> tags in a single response | |
| • After the final result, synthesize all outputs into one coherent answer | |
| ══════════════════════════════════════════ | |
| ABSOLUTE PROHIBITIONS | |
| ══════════════════════════════════════════ | |
| ✘ Never invent tool names not listed in AVAILABLE TOOLS above | |
| ✘ Never output partial, nested, or malformed XML | |
| ✘ Never mix tool XML with surrounding prose in the same response | |
| ✘ Never guess parameter values — if uncertain, trigger CASE D | |
| ✘ Never ask "Should I use a tool for this?" — decide silently | |
| ✘ Never mention that you have tools unless the user explicitly asks | |
| ✘ Never use a tool for rhetorical, emotional, or illustrative purposes | |
| ✘ Never retry a failed tool call with the same parameters — report the error instead | |
| """; | |
| public static string Build(IReadOnlyList<ToolDefinition> tools) | |
| { | |
| var block = new StringBuilder(); | |
| foreach (var tool in tools) | |
| { | |
| // Example call signature | |
| var paramStr = string.Join(" ", tool.Params.Select(p => $"{p.Name}=\"{{{p.Type}}}\"")); | |
| block.AppendLine($"• {tool.Name}: {tool.Description}"); | |
| block.AppendLine($" Signature: <call:{tool.Name} {paramStr} />"); | |
| block.AppendLine($" Parameters:"); | |
| foreach (var p in tool.Params) | |
| block.AppendLine($" - {p.Name} ({p.Type}): {p.Description}"); | |
| block.AppendLine(); | |
| } | |
| return BasePrompt.Replace("{TOOLS_BLOCK}", block.ToString().TrimEnd()); | |
| } | |
| } | |
| // ───────────────────────────────────────────────────────────── | |
| // TOOL EXECUTOR — dispatches XML tag → registered handler | |
| // ───────────────────────────────────────────────────────────── | |
| public static class ToolExecutor | |
| { | |
| private static readonly Regex ToolNameRx = new(@"<call:(\w+)", RegexOptions.Compiled); | |
| /// <summary>Returns null if no tool call is present in <paramref name="raw"/>.</summary> | |
| public static string? TryExecute(string raw) | |
| { | |
| var match = ToolNameRx.Match(raw); | |
| if (!match.Success) return null; | |
| string toolName = match.Groups[1].Value; | |
| if (!ToolRegistry.TryGet(toolName, out var tool)) | |
| return $"Error: Unknown tool '{toolName}'."; | |
| try | |
| { | |
| return tool!.Handler(raw); | |
| } | |
| catch (Exception ex) | |
| { | |
| return $"Error executing '{toolName}': {ex.Message}"; | |
| } | |
| } | |
| } | |
| // ───────────────────────────────────────────────────────────── | |
| // MAIN CHAT LOOP | |
| // ───────────────────────────────────────────────────────────── | |
| public class OllamaChat | |
| { | |
| const string OllamaUrl = "http://localhost:11434/api/chat"; | |
| const string ModelName = "qwen2.5:3b"; | |
| static readonly HttpClient Http = new(); | |
| static async Task Main() | |
| { | |
| // Build the system prompt dynamically from the registry | |
| string systemPrompt = PromptBuilder.Build(ToolRegistry.Tools); | |
| var history = new List<OllamaMessage> | |
| { | |
| new() { Role = "system", Content = systemPrompt } | |
| }; | |
| Console.WriteLine($"=== Engine Ready ({ModelName}) — {ToolRegistry.Tools.Count} tool(s) loaded ==="); | |
| foreach (var t in ToolRegistry.Tools) | |
| Console.WriteLine($" • {t.Name}"); | |
| Console.WriteLine(); | |
| while (true) | |
| { | |
| Console.Write("You: "); | |
| string? input = Console.ReadLine(); | |
| if (string.IsNullOrEmpty(input) || input == "exit") break; | |
| history.Add(new OllamaMessage { Role = "user", Content = input }); | |
| Console.Write("AI: "); | |
| string reply = await StreamAndCapture(history); | |
| Console.WriteLine(); | |
| string? toolResult = ToolExecutor.TryExecute(reply); | |
| if (toolResult != null) | |
| { | |
| Console.WriteLine($"⚙️ [Tool executed] → {toolResult}"); | |
| // Inject result back so the model can formulate a natural reply | |
| history.Add(new OllamaMessage { Role = "assistant", Content = reply }); | |
| history.Add(new OllamaMessage { Role = "user", Content = $"Result: {toolResult}" }); | |
| // Let the model turn the result into a natural response | |
| Console.Write("AI: "); | |
| string finalReply = await StreamAndCapture(history); | |
| Console.WriteLine(); | |
| history.Add(new OllamaMessage { Role = "assistant", Content = finalReply }); | |
| } | |
| else | |
| { | |
| history.Add(new OllamaMessage { Role = "assistant", Content = reply }); | |
| } | |
| } | |
| } | |
| static async Task<string> StreamAndCapture(List<OllamaMessage> history) | |
| { | |
| var sb = new StringBuilder(); | |
| var req = new OllamaRequest | |
| { | |
| Model = ModelName, | |
| Stream = true, | |
| Messages = history, | |
| Options = new OllamaOptions { Temperature = 0.0f } | |
| }; | |
| var json = JsonSerializer.Serialize(req, OllamaJsonContext.Default.OllamaRequest); | |
| using var resp = await Http.PostAsync( | |
| OllamaUrl, new StringContent(json, Encoding.UTF8, "application/json")); | |
| using var reader = new StreamReader(await resp.Content.ReadAsStreamAsync()); | |
| while (await reader.ReadLineAsync() is { } line) | |
| { | |
| var chunk = JsonSerializer.Deserialize(line, OllamaJsonContext.Default.OllamaResponse); | |
| if (chunk?.Message?.Content != null) | |
| { | |
| Console.Write(chunk.Message.Content); | |
| sb.Append(chunk.Message.Content); | |
| } | |
| if (chunk?.Done == true) break; | |
| } | |
| return sb.ToString(); | |
| } | |
| } | |
| // ───────────────────────────────────────────────────────────── | |
| // OLLAMA DATA MODELS | |
| // ───────────────────────────────────────────────────────────── | |
| public class OllamaOptions | |
| { | |
| [JsonPropertyName("temperature")] public float Temperature { get; set; } | |
| } | |
| public class OllamaRequest | |
| { | |
| [JsonPropertyName("model")] public string Model { get; set; } = ""; | |
| [JsonPropertyName("stream")] public bool Stream { get; set; } | |
| [JsonPropertyName("messages")] public List<OllamaMessage> Messages { get; set; } = new(); | |
| [JsonPropertyName("options")] public OllamaOptions? Options { get; set; } | |
| } | |
| public class OllamaMessage | |
| { | |
| [JsonPropertyName("role")] public string Role { get; set; } = ""; | |
| [JsonPropertyName("content")] public string Content { get; set; } = ""; | |
| } | |
| public class OllamaResponse | |
| { | |
| [JsonPropertyName("message")] public OllamaMessage? Message { get; set; } | |
| [JsonPropertyName("done")] public bool Done { get; set; } | |
| } | |
| ``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment