Skip to content

Instantly share code, notes, and snippets.

@artydev
Created June 14, 2026 09:50
Show Gist options
  • Select an option

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

Select an option

Save artydev/c9a5daa55be92be6e4ba5e63353780ed to your computer and use it in GitHub Desktop.
Engine Ready (qwen2.5:3b)
```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