mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-03-31 18:38:22 +00:00
LLM
This commit is contained in:
@@ -1,24 +1,51 @@
|
|||||||
using Esiur.Schema.Llm;
|
using Esiur.Resource;
|
||||||
|
using Esiur.Schema.Llm;
|
||||||
|
using Esiur.Stores;
|
||||||
using OpenAI;
|
using OpenAI;
|
||||||
using OpenAI.Chat;
|
using OpenAI.Chat;
|
||||||
using System;
|
using System;
|
||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Esiur.Tests.Annotations
|
namespace Esiur.Tests.Annotations;
|
||||||
|
|
||||||
|
//public sealed class TickState
|
||||||
|
//{
|
||||||
|
// public int Load { get; set; }
|
||||||
|
// public int ErrorCount { get; set; }
|
||||||
|
// public bool Enabled { get; set; }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//public sealed class LlmDecision
|
||||||
|
//{
|
||||||
|
// public string? Function { get; set; }
|
||||||
|
// public string? Reason { get; set; }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class LlmRunner
|
||||||
{
|
{
|
||||||
public class LlmRunner
|
private static readonly HashSet<string?> ValidFunctions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
public async Task RunAsync(ServiceNode node, string endpoint, ApiKeyCredential apiKey, string modelName,
|
null, "Restart", "ResetErrors", "Enable", "Disable"
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task<(List<TickResult> Results, List<ModelSummary> Summary)> RunAsync(
|
||||||
|
|
||||||
|
IReadOnlyList<ModelConfig> models,
|
||||||
int tickDelayMs = 1000)
|
int tickDelayMs = 1000)
|
||||||
{
|
{
|
||||||
var client = new OpenAIClient(apiKey, new OpenAIClientOptions() { Endpoint = new Uri(endpoint) });
|
|
||||||
var chat = client.GetChatClient("microsoft/phi-4");
|
|
||||||
|
|
||||||
var typeModel = LlmTypeModel.FromTypeDef(node.Instance?.Definition);
|
var wh = new Warehouse();
|
||||||
|
|
||||||
|
await wh.Put("store", new MemoryStore());
|
||||||
|
|
||||||
|
var allResults = new List<TickResult>();
|
||||||
|
|
||||||
var ticks = new List<TickState>
|
var ticks = new List<TickState>
|
||||||
{
|
{
|
||||||
@@ -30,102 +57,209 @@ namespace Esiur.Tests.Annotations
|
|||||||
new() { Load = 25, ErrorCount = 0, Enabled = true }
|
new() { Load = 25, ErrorCount = 0, Enabled = true }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var expectations = new List<TickExpectation>
|
||||||
|
{
|
||||||
|
new() { Tick = 1, AllowedFunctions = new HashSet<string?> { null }, Note = "Stable service; no action expected." },
|
||||||
|
new() { Tick = 2, AllowedFunctions = new HashSet<string?> { "Restart" }, Note = "Overload; restart expected." },
|
||||||
|
new() { Tick = 3, AllowedFunctions = new HashSet<string?> { "Restart", "ResetErrors" }, Note = "High error count; restart or reset is acceptable." },
|
||||||
|
new() { Tick = 4, AllowedFunctions = new HashSet<string?> { "Enable" }, Note = "Service disabled; enable expected." },
|
||||||
|
new() { Tick = 5, AllowedFunctions = new HashSet<string?> { "Restart" }, Note = "Overload and instability; restart expected." },
|
||||||
|
new() { Tick = 6, AllowedFunctions = new HashSet<string?> { null }, Note = "Stable service; no action expected." }
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var model in models)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"=== Model: {model.Name} ({model.ModelName}) ===");
|
||||||
|
|
||||||
|
var client = new OpenAIClient(
|
||||||
|
model.ApiKey,
|
||||||
|
new OpenAIClientOptions { Endpoint = new Uri(model.Endpoint) });
|
||||||
|
|
||||||
|
var chat = client.GetChatClient(model.ModelName);
|
||||||
|
|
||||||
|
Console.WriteLine($"Warming up {model.Name}...");
|
||||||
|
|
||||||
|
await InferAsync(chat,
|
||||||
|
"Return {\"function\":null,\"reason\":\"warmup\"}");
|
||||||
|
|
||||||
|
Console.WriteLine("Warmup done");
|
||||||
|
|
||||||
|
// Fresh node instance per model so results are independent.
|
||||||
|
var node = await wh.Put("store/service-" + model.Name, new ServiceNode());
|
||||||
|
|
||||||
|
var typeModel = LlmTypeModel.FromTypeDef(node.Instance?.Definition);
|
||||||
|
|
||||||
for (int i = 0; i < ticks.Count; i++)
|
for (int i = 0; i < ticks.Count; i++)
|
||||||
{
|
{
|
||||||
var tick = ticks[i];
|
var tick = ticks[i];
|
||||||
|
var expected = expectations[i];
|
||||||
|
|
||||||
// Simulate property changes for this tick
|
// Apply tick state before inference
|
||||||
node.Load = tick.Load;
|
node.Load = tick.Load;
|
||||||
node.ErrorCount = tick.ErrorCount;
|
node.ErrorCount = tick.ErrorCount;
|
||||||
node.Enabled = tick.Enabled;
|
node.Enabled = tick.Enabled;
|
||||||
|
|
||||||
|
var loadBefore = node.Load;
|
||||||
|
var errorBefore = node.ErrorCount;
|
||||||
|
var enabledBefore = node.Enabled;
|
||||||
|
|
||||||
var jsonModel = typeModel.ToJson(node);
|
var jsonModel = typeModel.ToJson(node);
|
||||||
Console.WriteLine($"Tick {i + 1}");
|
var prompt = BuildPrompt(jsonModel, i + 1);
|
||||||
Console.WriteLine($"State: Load={node.Load}, ErrorCount={node.ErrorCount}, Enabled={node.Enabled}");
|
|
||||||
|
|
||||||
var prompt = BuildPrompt(jsonModel, node, i + 1);
|
var sw = Stopwatch.StartNew();
|
||||||
|
string raw = await InferAsync(chat, prompt);
|
||||||
|
sw.Stop();
|
||||||
|
|
||||||
string llmRaw = await InferAsync(chat, prompt);
|
var parsedResult = ParseDecisionWithRepair(raw);
|
||||||
var decision = ParseDecision(llmRaw);
|
|
||||||
|
|
||||||
bool invoked = InvokeIfValid(node, decision?.Function);
|
var firstDecision = parsedResult.First;
|
||||||
|
var finalDecision = parsedResult.Final;
|
||||||
|
|
||||||
Console.WriteLine($"LLM: {llmRaw}");
|
var parsed = finalDecision != null;
|
||||||
Console.WriteLine($"Invoked: {invoked}");
|
var repaired = parsedResult.Repaired;
|
||||||
Console.WriteLine($"After: Load={node.Load}, ErrorCount={node.ErrorCount}, Enabled={node.Enabled}");
|
var jsonObjectCount = parsedResult.Count;
|
||||||
Console.WriteLine(new string('-', 60));
|
|
||||||
|
|
||||||
|
var firstPredicted = NormalizeFunction(firstDecision?.Function);
|
||||||
|
var predicted = NormalizeFunction(finalDecision?.Function);
|
||||||
|
|
||||||
|
var allowed = ValidFunctions.Contains(predicted);
|
||||||
|
var correct = expected.AllowedFunctions.Contains(predicted);
|
||||||
|
|
||||||
|
var invoked = false;
|
||||||
|
if (allowed)
|
||||||
|
invoked = InvokeIfValid(node, predicted);
|
||||||
|
|
||||||
|
var result = new TickResult
|
||||||
|
{
|
||||||
|
Model = model.Name,
|
||||||
|
Tick = i + 1,
|
||||||
|
|
||||||
|
LoadBefore = loadBefore,
|
||||||
|
ErrorCountBefore = errorBefore,
|
||||||
|
EnabledBefore = enabledBefore,
|
||||||
|
|
||||||
|
RawResponse = raw,
|
||||||
|
FirstPredictedFunction = firstPredicted,
|
||||||
|
PredictedFunction = predicted,
|
||||||
|
Reason = finalDecision?.Reason,
|
||||||
|
|
||||||
|
Parsed = parsed,
|
||||||
|
Allowed = allowed,
|
||||||
|
Correct = correct,
|
||||||
|
Repaired = repaired,
|
||||||
|
JsonObjectCount = jsonObjectCount,
|
||||||
|
Invoked = invoked,
|
||||||
|
LatencyMs = sw.Elapsed.TotalMilliseconds,
|
||||||
|
|
||||||
|
LoadAfter = node.Load,
|
||||||
|
ErrorCountAfter = node.ErrorCount,
|
||||||
|
EnabledAfter = node.Enabled,
|
||||||
|
|
||||||
|
ExpectedText = string.Join(" | ", expected.AllowedFunctions.Select(x => x ?? "null"))
|
||||||
|
};
|
||||||
|
|
||||||
|
allResults.Add(result);
|
||||||
|
|
||||||
|
Console.WriteLine($"Tick {result.Tick}");
|
||||||
|
Console.WriteLine($"Before: Load={result.LoadBefore}, ErrorCount={result.ErrorCountBefore}, Enabled={result.EnabledBefore}");
|
||||||
|
Console.WriteLine($"Expected: {result.ExpectedText}");
|
||||||
|
Console.WriteLine($"LLM: {result.RawResponse}");
|
||||||
|
Console.WriteLine($"First: {result.FirstPredictedFunction ?? "null"}");
|
||||||
|
Console.WriteLine($"Final: {result.PredictedFunction ?? "null"}");
|
||||||
|
Console.WriteLine($"Parsed={result.Parsed}, Allowed={result.Allowed}, Correct={result.Correct}, Repaired={result.Repaired}, Invoked={result.Invoked}, Latency={result.LatencyMs:F1} ms");
|
||||||
|
Console.WriteLine($"After: Load={result.LoadAfter}, ErrorCount={result.ErrorCountAfter}, Enabled={result.EnabledAfter}");
|
||||||
|
Console.WriteLine(new string('-', 72));
|
||||||
await Task.Delay(tickDelayMs);
|
await Task.Delay(tickDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<string> InferAsync(
|
var summary = Summarize(allResults);
|
||||||
ChatClient chat,
|
return (allResults, summary);
|
||||||
string prompt)
|
}
|
||||||
{
|
|
||||||
|
|
||||||
List<ChatMessage> messages = new List<ChatMessage>
|
private static async Task<string> InferAsync(ChatClient chat, string prompt)
|
||||||
{
|
{
|
||||||
new SystemChatMessage("You control a distributed resource. " +
|
List<ChatMessage> messages = new()
|
||||||
"Return only JSON with fields: function and reason."),
|
{
|
||||||
|
new SystemChatMessage(
|
||||||
|
"You control a distributed resource. " +
|
||||||
|
"Return raw JSON only with fields: function and reason. " +
|
||||||
|
"Do not wrap the response in markdown or code fences."),
|
||||||
new UserChatMessage(prompt)
|
new UserChatMessage(prompt)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await chat.CompleteChatAsync(messages);
|
var result = await chat.CompleteChatAsync(messages);
|
||||||
|
|
||||||
return result.Value.Content[0].Text;
|
return result.Value.Content[0].Text;
|
||||||
}
|
}
|
||||||
private static string BuildPrompt(string typeDefJson, ServiceNode node, int tick)
|
|
||||||
|
private static string BuildPrompt(string typeDefJson, int tick)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
$@"You are given a runtime type definition for a distributed resource and its current state.
|
$@"You are given a runtime type definition for a distributed resource and its current state.
|
||||||
Choose at most one function to call.
|
Choose at most one function to call.
|
||||||
Use only the functions defined in the type definition.
|
Use only the functions defined in the type definition.
|
||||||
Do not invent functions.
|
Do not invent functions.
|
||||||
If no action is needed, return function as null.
|
Return ONLY valid JSON in this format:
|
||||||
Return only JSON in this format:
|
{{ ""function"": ""<<name>>"", ""reason"": ""short explanation"" }}
|
||||||
{{ ""function"": ""Restart|ResetErrors|Enable|Disable|null"", ""reason"": ""short explanation"" }}
|
If the current state is normal and no action is needed, return:
|
||||||
|
{{ ""function"": null, ""reason"": ""..."" }}.
|
||||||
|
|
||||||
Type Definition:
|
Input:
|
||||||
{typeDefJson}";
|
{typeDefJson}";
|
||||||
|
|
||||||
//Current Tick: {tick}
|
|
||||||
//Current State:
|
|
||||||
//{{
|
|
||||||
// ""Load"": {node.Load},
|
|
||||||
// ""ErrorCount"": {node.ErrorCount},
|
|
||||||
// ""Enabled"": {(node.Enabled ? "true" : "false")}
|
|
||||||
//}}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LlmDecision? ParseDecision(string text)
|
//private static LlmDecision? ParseDecision(string text)
|
||||||
{
|
//{
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
var json = ExtractJson(text);
|
// var json = ExtractJson(text);
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<LlmDecision>(
|
// return JsonSerializer.Deserialize<LlmDecision>(
|
||||||
json,
|
// json,
|
||||||
new JsonSerializerOptions
|
// new JsonSerializerOptions
|
||||||
|
// {
|
||||||
|
// PropertyNameCaseInsensitive = true
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
private static (LlmDecision? First, LlmDecision? Final, bool Repaired, int Count) ParseDecisionWithRepair(string text)
|
||||||
|
{
|
||||||
|
var objects = ExtractJsonObjects(text);
|
||||||
|
|
||||||
|
if (objects.Count == 0)
|
||||||
|
return (null, null, false, 0);
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true
|
PropertyNameCaseInsensitive = true
|
||||||
});
|
};
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ExtractJson(string text)
|
LlmDecision? first = null;
|
||||||
|
LlmDecision? final = null;
|
||||||
|
|
||||||
|
try { first = JsonSerializer.Deserialize<LlmDecision>(objects[0], options); } catch { }
|
||||||
|
try { final = JsonSerializer.Deserialize<LlmDecision>(objects[^1], options); } catch { }
|
||||||
|
|
||||||
|
bool repaired = objects.Count > 1 &&
|
||||||
|
NormalizeFunction(first?.Function) != NormalizeFunction(final?.Function);
|
||||||
|
|
||||||
|
return (first, final, repaired, objects.Count);
|
||||||
|
}
|
||||||
|
private static List<string> ExtractJsonObjects(string text)
|
||||||
{
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
return "{}";
|
return results;
|
||||||
|
|
||||||
text = text.Trim();
|
text = text.Trim();
|
||||||
|
|
||||||
if (text.StartsWith("```"))
|
if (text.StartsWith("```", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var firstNewline = text.IndexOf('\n');
|
var firstNewline = text.IndexOf('\n');
|
||||||
if (firstNewline >= 0)
|
if (firstNewline >= 0)
|
||||||
@@ -136,13 +270,76 @@ Type Definition:
|
|||||||
text = text[..lastFence];
|
text = text[..lastFence];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int depth = 0;
|
||||||
|
int start = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char c = text[i];
|
||||||
|
|
||||||
|
if (c == '{')
|
||||||
|
{
|
||||||
|
if (depth == 0)
|
||||||
|
start = i;
|
||||||
|
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
else if (c == '}')
|
||||||
|
{
|
||||||
|
if (depth > 0)
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
|
||||||
|
if (depth == 0 && start >= 0)
|
||||||
|
{
|
||||||
|
results.Add(text.Substring(start, i - start + 1));
|
||||||
|
start = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
private static string ExtractJson(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return "{}";
|
||||||
|
|
||||||
|
text = text.Trim();
|
||||||
|
|
||||||
|
if (text.StartsWith("```", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var firstNewline = text.IndexOf('\n');
|
||||||
|
if (firstNewline >= 0)
|
||||||
|
text = text[(firstNewline + 1)..];
|
||||||
|
|
||||||
|
var lastFence = text.LastIndexOf("```", StringComparison.Ordinal);
|
||||||
|
if (lastFence >= 0)
|
||||||
|
text = text[..lastFence];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: extract first JSON object if extra text exists.
|
||||||
|
int start = text.IndexOf('{');
|
||||||
|
int end = text.LastIndexOf('}');
|
||||||
|
if (start >= 0 && end > start)
|
||||||
|
text = text.Substring(start, end - start + 1);
|
||||||
|
|
||||||
return text.Trim();
|
return text.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeFunction(string? functionName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(functionName) ||
|
||||||
|
string.Equals(functionName, "null", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return functionName.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private static bool InvokeIfValid(ServiceNode node, string? functionName)
|
private static bool InvokeIfValid(ServiceNode node, string? functionName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(functionName) ||
|
if (functionName == null)
|
||||||
string.Equals(functionName, "null", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
switch (functionName)
|
switch (functionName)
|
||||||
@@ -167,5 +364,47 @@ Type Definition:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ModelSummary> Summarize(List<TickResult> results)
|
||||||
|
{
|
||||||
|
return results
|
||||||
|
.GroupBy(r => r.Model)
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
var latencies = g.Select(x => x.LatencyMs).OrderBy(x => x).ToList();
|
||||||
|
|
||||||
|
return new ModelSummary
|
||||||
|
{
|
||||||
|
Model = g.Key,
|
||||||
|
TotalTicks = g.Count(),
|
||||||
|
ParseRate = 100.0 * g.Count(x => x.Parsed) / g.Count(),
|
||||||
|
AllowedRate = 100.0 * g.Count(x => x.Allowed) / g.Count(),
|
||||||
|
CorrectRate = 100.0 * g.Count(x => x.Correct) / g.Count(),
|
||||||
|
MeanLatencyMs = g.Average(x => x.LatencyMs),
|
||||||
|
P95LatencyMs = Percentile(latencies, 0.95),
|
||||||
|
RepairRate = 100.0 * g.Count(x => x.Repaired) / g.Count(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.OrderBy(x => x.Model)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double Percentile(List<double> sortedValues, double p)
|
||||||
|
{
|
||||||
|
if (sortedValues.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (sortedValues.Count == 1)
|
||||||
|
return sortedValues[0];
|
||||||
|
|
||||||
|
double index = (sortedValues.Count - 1) * p;
|
||||||
|
int lower = (int)Math.Floor(index);
|
||||||
|
int upper = (int)Math.Ceiling(index);
|
||||||
|
|
||||||
|
if (lower == upper)
|
||||||
|
return sortedValues[lower];
|
||||||
|
|
||||||
|
double weight = index - lower;
|
||||||
|
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
Tests/Annotations/ModelConfig.cs
Normal file
15
Tests/Annotations/ModelConfig.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.ClientModel;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Annotations
|
||||||
|
{
|
||||||
|
public sealed class ModelConfig
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Endpoint { get; set; } = "";
|
||||||
|
public ApiKeyCredential ApiKey { get; set; } = default!;
|
||||||
|
public string ModelName { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Tests/Annotations/ModelSummary.cs
Normal file
21
Tests/Annotations/ModelSummary.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Annotations
|
||||||
|
{
|
||||||
|
public sealed class ModelSummary
|
||||||
|
{
|
||||||
|
public string Model { get; set; } = "";
|
||||||
|
public int TotalTicks { get; set; }
|
||||||
|
|
||||||
|
public double ParseRate { get; set; }
|
||||||
|
public double AllowedRate { get; set; }
|
||||||
|
public double CorrectRate { get; set; }
|
||||||
|
|
||||||
|
public double MeanLatencyMs { get; set; }
|
||||||
|
public double P95LatencyMs { get; set; }
|
||||||
|
|
||||||
|
public double RepairRate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,38 +7,75 @@ using OpenAI.Chat;
|
|||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
var wh = new Warehouse();
|
|
||||||
|
|
||||||
await wh.Put("store", new MemoryStore());
|
|
||||||
var node = await wh.Put("store/service", new ServiceNode());
|
|
||||||
|
|
||||||
var endpoint = "http://localhost:1234/v1";
|
var endpoint = "http://localhost:1234/v1";
|
||||||
var credential = new ApiKeyCredential("lm-studio");
|
var credential = new ApiKeyCredential("lm-studio");
|
||||||
|
|
||||||
//var client = new OpenAIClient(credential, new OpenAIClientOptions() { Endpoint = new Uri(endpoint) });
|
////var client = new OpenAIClient(credential, new OpenAIClientOptions() { Endpoint = new Uri(endpoint) });
|
||||||
|
|
||||||
//var chat = client.GetChatClient("microsoft/phi-4");
|
////var chat = client.GetChatClient("microsoft/phi-4");
|
||||||
|
|
||||||
var llmRunner = new LlmRunner();
|
//var llmRunner = new LlmRunner();
|
||||||
|
|
||||||
await llmRunner.RunAsync(
|
//await llmRunner.RunAsync(
|
||||||
node,
|
// node,
|
||||||
endpoint,
|
// endpoint,
|
||||||
credential,
|
// credential,
|
||||||
"microsoft/phi-4"
|
// "microsoft/phi-4"
|
||||||
);
|
|
||||||
|
|
||||||
//List<ChatMessage> messages = new List<ChatMessage>
|
|
||||||
//{
|
|
||||||
// new SystemChatMessage("You are a helpful assistant that only speaks in rhymes."),
|
|
||||||
// new UserChatMessage("What is the capital of France?")
|
|
||||||
//};
|
|
||||||
|
|
||||||
//// Send the entire conversation history
|
|
||||||
//ChatCompletion completion = chat.CompleteChat(messages);
|
|
||||||
|
|
||||||
//var response = await chat.CompleteChatAsync(
|
|
||||||
// "Explain what Pi means"
|
|
||||||
//);
|
//);
|
||||||
|
|
||||||
//Console.WriteLine(response.Value.Content[0].Text);
|
var runner = new LlmRunner();
|
||||||
|
|
||||||
|
var models = new List<ModelConfig>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Phi-4",
|
||||||
|
Endpoint = "http://localhost:1234/v1",
|
||||||
|
ApiKey = new ApiKeyCredential("lm-studio"),
|
||||||
|
ModelName = "microsoft/phi-4"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Qwen2.5-7B",
|
||||||
|
Endpoint = "http://localhost:1234/v1",
|
||||||
|
ApiKey = new ApiKeyCredential("lm-studio"),
|
||||||
|
ModelName = "qwen2.5-7b-instruct"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "gpt-oss",
|
||||||
|
Endpoint = "http://localhost:1234/v1",
|
||||||
|
ApiKey = new ApiKeyCredential("lm-studio"),
|
||||||
|
ModelName = "openai/gpt-oss-20b"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "qwen2.5-1.5b-instruct",
|
||||||
|
Endpoint = "http://localhost:1234/v1",
|
||||||
|
ApiKey = new ApiKeyCredential("lm-studio"),
|
||||||
|
ModelName = "qwen2.5-1.5b-instruct"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "ministral-3-3b",
|
||||||
|
Endpoint = "http://localhost:1234/v1",
|
||||||
|
ApiKey = new ApiKeyCredential("lm-studio"),
|
||||||
|
ModelName = "mistralai/ministral-3-3b"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "deepseek-r1-0528-qwen3-8b",
|
||||||
|
Endpoint = "http://localhost:1234/v1",
|
||||||
|
ApiKey = new ApiKeyCredential("lm-studio"),
|
||||||
|
ModelName = "deepseek/deepseek-r1-0528-qwen3-8b"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var (results, summary) = await runner.RunAsync( models.Skip(5).Take(1).ToArray(),
|
||||||
|
250);
|
||||||
|
|
||||||
|
foreach (var item in summary)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{item.Model}: Correct={item.CorrectRate:F1}% Repair={item.RepairRate:F1}% Mean={item.MeanLatencyMs:F1} ms P95={item.P95LatencyMs:F1} ms");
|
||||||
|
}
|
||||||
@@ -9,14 +9,15 @@ namespace Esiur.Tests.Annotations
|
|||||||
[Annotation("usage_rules", @"1.Choose at most one function per tick.
|
[Annotation("usage_rules", @"1.Choose at most one function per tick.
|
||||||
2. Use only functions defined in the functions list.
|
2. Use only functions defined in the functions list.
|
||||||
3. Do not invent properties or functions.
|
3. Do not invent properties or functions.
|
||||||
4. Base the decision only on current property values and annotations.")]
|
4. Base the decision only on current property values and annotations.
|
||||||
|
5. Keep the service enabled as much as possible")]
|
||||||
[Resource]
|
[Resource]
|
||||||
public partial class ServiceNode
|
public partial class ServiceNode
|
||||||
{
|
{
|
||||||
[Annotation("Current service load percentage from 0 to 100. Values above 80 indicate overload.")]
|
[Annotation("Current service load percentage from 0 to 100. Values above 80 indicate overload.")]
|
||||||
[Export] int load;
|
[Export] int load;
|
||||||
|
|
||||||
[Annotation("Number of recent errors detected in the service. Values above 3 indicate instability.")]
|
[Annotation("Number of recent errors detected in the service. Values above 3 indicate instability. A value of 0 means no reset is needed")]
|
||||||
[Export] int errorCount;
|
[Export] int errorCount;
|
||||||
|
|
||||||
[Annotation("True when the service is enabled and allowed to run. False means the service is disabled.")]
|
[Annotation("True when the service is enabled and allowed to run. False means the service is disabled.")]
|
||||||
@@ -30,13 +31,13 @@ namespace Esiur.Tests.Annotations
|
|||||||
Enabled = true;
|
Enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Annotation("Clear the error counter when errors were temporary and a restart is not required.")]
|
[Annotation("Clear recent errors only when ErrorCount is greater than 0 and the service is otherwise stable.")]
|
||||||
[Export] public void ResetErrors()
|
[Export] public void ResetErrors()
|
||||||
{
|
{
|
||||||
ErrorCount = 0;
|
ErrorCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Annotation("Enable the service if it is disabled and should be running.")]
|
[Annotation("Enable the service when Enabled is false.")]
|
||||||
[Export] public void Enable()
|
[Export] public void Enable()
|
||||||
{
|
{
|
||||||
Enabled = true;
|
Enabled = true;
|
||||||
|
|||||||
13
Tests/Annotations/TickExpectation.cs
Normal file
13
Tests/Annotations/TickExpectation.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Annotations
|
||||||
|
{
|
||||||
|
public sealed class TickExpectation
|
||||||
|
{
|
||||||
|
public int Tick { get; set; }
|
||||||
|
public HashSet<string?> AllowedFunctions { get; set; } = new();
|
||||||
|
public string Note { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Tests/Annotations/TickResult.cs
Normal file
42
Tests/Annotations/TickResult.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Annotations
|
||||||
|
{
|
||||||
|
public sealed class TickResult
|
||||||
|
{
|
||||||
|
public string Model { get; set; } = "";
|
||||||
|
public int Tick { get; set; }
|
||||||
|
|
||||||
|
public int LoadBefore { get; set; }
|
||||||
|
public int ErrorCountBefore { get; set; }
|
||||||
|
public bool EnabledBefore { get; set; }
|
||||||
|
|
||||||
|
public string RawResponse { get; set; } = "";
|
||||||
|
public string? PredictedFunction { get; set; }
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
|
||||||
|
public bool Parsed { get; set; }
|
||||||
|
public bool Allowed { get; set; }
|
||||||
|
public bool Invoked { get; set; }
|
||||||
|
public bool Correct { get; set; }
|
||||||
|
|
||||||
|
public double LatencyMs { get; set; }
|
||||||
|
|
||||||
|
public int LoadAfter { get; set; }
|
||||||
|
public int ErrorCountAfter { get; set; }
|
||||||
|
public bool EnabledAfter { get; set; }
|
||||||
|
|
||||||
|
public string ExpectedText { get; set; } = "";
|
||||||
|
|
||||||
|
|
||||||
|
public bool Repaired { get; set; }
|
||||||
|
public int JsonObjectCount { get; set; }
|
||||||
|
public string? FirstFunction { get; set; }
|
||||||
|
public string? FinalFunction { get; set; }
|
||||||
|
|
||||||
|
public string? FirstPredictedFunction { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,5 +10,4 @@ namespace Esiur.Tests.Annotations
|
|||||||
public int ErrorCount { get; set; }
|
public int ErrorCount { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user