初始提交

This commit is contained in:
2026-08-26 18:06:02 +08:00
commit 5544788917
53 changed files with 4101 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
using System.Text.Json;
namespace MAF1.Agents;
public sealed class AgentStepResult
{
public string Message { get; init; } = "";
public Dictionary<string, object?> Inputs { get; init; } = new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, object?> Outputs { get; init; } = new(StringComparer.OrdinalIgnoreCase);
}
internal static class AgentInputs
{
public static string? ReadString(IReadOnlyDictionary<string, object?> inputs, string key)
{
if (!inputs.TryGetValue(key, out object? value) || value is null)
{
return null;
}
if (value is JsonElement el)
{
return el.ValueKind == JsonValueKind.String ? el.GetString() : el.ToString();
}
return value.ToString();
}
public static List<string> ReadStringList(IReadOnlyDictionary<string, object?> inputs, string key)
{
if (!inputs.TryGetValue(key, out object? value) || value is null)
{
return [];
}
if (value is JsonElement el)
{
if (el.ValueKind == JsonValueKind.Array)
{
return el.EnumerateArray()
.Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() : item.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s!.Trim())
.ToList();
}
if (el.ValueKind == JsonValueKind.String)
{
return Split(el.GetString());
}
}
if (value is IEnumerable<string> typed)
{
return typed.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToList();
}
if (value is IEnumerable<object> objects)
{
return objects.Select(o => o?.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s!.Trim())
.ToList();
}
return Split(value.ToString());
}
private static List<string> Split(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return [];
}
return text.Split(['、', ',', ';', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
}
}
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace MAF1.Agents.FileCity;
public sealed class CityExtraction
{
[JsonPropertyName("hasValidCities")]
public bool HasValidCities { get; set; }
[JsonPropertyName("cities")]
public List<string> Cities { get; set; } = [];
[JsonPropertyName("reason")]
public string Reason { get; set; } = "";
}
@@ -0,0 +1,81 @@
using System.Text.Json;
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace MAF1.Agents.FileCity;
public static class FileCityAgent
{
public static AIAgent Create(AgentFactory factory)
{
return factory.CreateAgent(
name: "FileCityAgent",
instructions:
"""
ReadTextFile
Amsterdam
JSON Markdown
{"hasValidCities": true, "cities": ["成都"], "reason": "说明"}
{"hasValidCities": false, "cities": [], "reason": "原因"}
cities
""",
tools: [AIFunctionFactory.Create(FileTools.ReadTextFile)]);
}
public static async Task<AgentStepResult> RunAsync(
AIAgent agent,
IReadOnlyDictionary<string, object?> inputs,
CancellationToken cancellationToken = default)
{
string filePath = AgentInputs.ReadString(inputs, "filePath") ?? "Data/cities.txt";
AgentResponse response = await agent.RunAsync(
$"请读取这个文件并提取有效城市名:{filePath}",
cancellationToken: cancellationToken);
CityExtraction extraction = Parse(response.Text);
return new AgentStepResult
{
Message = response.Text,
Inputs = new Dictionary<string, object?> { ["filePath"] = filePath },
Outputs = new Dictionary<string, object?>
{
["hasValidCities"] = extraction.HasValidCities,
["cities"] = extraction.Cities,
["reason"] = extraction.Reason,
["raw"] = response.Text,
},
};
}
public static CityExtraction Parse(string text)
{
try
{
CityExtraction? result = JsonSerializer.Deserialize<CityExtraction>(
JsonText.UnwrapObject(text),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (result is null)
{
return Invalid("无法解析文件 Agent 的输出。");
}
result.Cities = result.Cities
.Where(city => !string.IsNullOrWhiteSpace(city))
.Select(city => city.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
result.HasValidCities = result.HasValidCities && result.Cities.Count > 0;
return result;
}
catch (JsonException ex)
{
return Invalid($"文件 Agent 没有返回合法 JSON{ex.Message}");
}
}
private static CityExtraction Invalid(string reason) =>
new() { HasValidCities = false, Reason = reason };
}
+43
View File
@@ -0,0 +1,43 @@
using MAF1.Tools;
using MAF1.Utils;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace MAF1.Agents.Weather;
public static class WeatherAgent
{
public static AIAgent Create(AgentFactory factory, WeatherTools weatherTools)
{
return factory.CreateAgent(
name: "WeatherAgent",
instructions:
"""
GetWeather
""",
tools: [AIFunctionFactory.Create(weatherTools.GetWeather)]);
}
public static async Task<AgentStepResult> RunAsync(
AIAgent agent,
IReadOnlyDictionary<string, object?> inputs,
CancellationToken cancellationToken = default)
{
List<string> cities = AgentInputs.ReadStringList(inputs, "cities");
if (cities.Count == 0)
{
throw new InvalidOperationException("输入 cities 为空。请把上一节点的 cities 连到本节点的 cities。");
}
AgentResponse response = await agent.RunAsync(
$"请查询这些城市的天气:{string.Join("", cities)}",
cancellationToken: cancellationToken);
return new AgentStepResult
{
Message = response.Text,
Inputs = new Dictionary<string, object?> { ["cities"] = cities },
Outputs = new Dictionary<string, object?> { ["summary"] = response.Text },
};
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>MAF1</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.19.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,68 @@
using System.Text.Json.Serialization;
namespace MAF1.PluginContract;
public sealed class PluginManifest
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "1.0.0";
public PluginLaunch Launch { get; set; } = new();
public int TimeoutSeconds { get; set; }
public Dictionary<string, string> Env { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<PluginCredentialNeed> Credentials { get; set; } = [];
public List<PluginPort> Inputs { get; set; } = [];
public List<PluginPort> Outputs { get; set; } = [];
}
public sealed class PluginLaunch
{
public string Command { get; set; } = "";
public List<string> Args { get; set; } = [];
}
public sealed class PluginPort
{
public string Name { get; set; } = "";
public string Type { get; set; } = "string";
public string Description { get; set; } = "";
public bool Required { get; set; }
}
public sealed class PluginCredentialNeed
{
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public bool Required { get; set; } = true;
public string Description { get; set; } = "";
}
public sealed class PluginRequest
{
public Dictionary<string, object?> Inputs { get; set; } = new(StringComparer.OrdinalIgnoreCase);
[JsonPropertyName("credentials")]
public Dictionary<string, PluginCredentialPayload> Credentials { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}
public sealed class PluginCredentialPayload
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string? Endpoint { get; set; }
public string? ApiKey { get; set; }
public string? Model { get; set; }
public Dictionary<string, string> Extra { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}
public static class PluginJson
{
public static readonly System.Text.Json.JsonSerializerOptions Options = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
}
+143
View File
@@ -0,0 +1,143 @@
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
namespace MAF1.PluginContract;
public static class PluginStdio
{
public static async Task<PluginRequest> ReadRequestAsync(CancellationToken cancellationToken = default)
{
using Stream stdin = Console.OpenStandardInput();
using StreamReader reader = new(stdin, Encoding.UTF8);
string json = await reader.ReadToEndAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(json))
{
throw new InvalidOperationException("插件没有从标准输入读到 JSON 请求。");
}
PluginRequest? request = JsonSerializer.Deserialize<PluginRequest>(json, PluginJson.Options);
return request ?? new PluginRequest();
}
public static async Task WriteOutputsAsync(Dictionary<string, object?> outputs, CancellationToken cancellationToken = default)
{
string json = JsonSerializer.Serialize(outputs, PluginJson.Options);
await Console.Out.WriteAsync(json.AsMemory(), cancellationToken);
await Console.Out.FlushAsync(cancellationToken);
}
public static void ApplyCredentialsToEnvironment(IReadOnlyDictionary<string, PluginCredentialPayload> credentials)
{
foreach (KeyValuePair<string, PluginCredentialPayload> pair in credentials)
{
PluginCredentialPayload cred = pair.Value;
if (cred.Type is "openai-compatible" or "llm" || pair.Key.Equals("llm", StringComparison.OrdinalIgnoreCase))
{
SetIfNotEmpty("OPENAI_ENDPOINT", cred.Endpoint);
SetIfNotEmpty("OPENAI_API_KEY", cred.ApiKey);
SetIfNotEmpty("OPENAI_CHAT_MODEL", cred.Model);
}
foreach (KeyValuePair<string, string> extra in cred.Extra)
{
if (!string.IsNullOrWhiteSpace(extra.Value))
{
Environment.SetEnvironmentVariable(extra.Key, extra.Value);
}
}
}
}
public static IConfiguration LoadHostConfiguration()
{
string contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT");
if (string.IsNullOrWhiteSpace(contentRoot) || !Directory.Exists(contentRoot))
{
contentRoot = AppContext.BaseDirectory;
DirectoryInfo? parent = Directory.GetParent(contentRoot.TrimEnd(Path.DirectorySeparatorChar));
if (parent?.Name.Equals("plugins", StringComparison.OrdinalIgnoreCase) == false
&& parent?.Parent is not null
&& File.Exists(Path.Combine(parent.Parent.FullName, "appsettings.json")))
{
contentRoot = parent.Parent.FullName;
}
else if (parent is not null && File.Exists(Path.Combine(parent.FullName, "appsettings.json")))
{
contentRoot = parent.FullName;
}
}
return new ConfigurationBuilder()
.SetBasePath(contentRoot)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
.AddEnvironmentVariables()
.Build();
}
public static string? ReadString(Dictionary<string, object?> inputs, string key)
{
if (!inputs.TryGetValue(key, out object? value) || value is null)
{
return null;
}
if (value is JsonElement el)
{
return el.ValueKind == JsonValueKind.String ? el.GetString() : el.ToString();
}
return value.ToString();
}
public static List<string> ReadStringList(Dictionary<string, object?> inputs, string key)
{
if (!inputs.TryGetValue(key, out object? value) || value is null)
{
return [];
}
if (value is JsonElement el)
{
if (el.ValueKind == JsonValueKind.Array)
{
return el.EnumerateArray()
.Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() : item.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s!.Trim())
.ToList();
}
if (el.ValueKind == JsonValueKind.String)
{
return Split(el.GetString());
}
}
if (value is IEnumerable<string> typed)
{
return typed.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToList();
}
return Split(value.ToString());
}
private static List<string> Split(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return [];
}
return text.Split(['、', ',', ';', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
}
private static void SetIfNotEmpty(string name, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
{
Environment.SetEnvironmentVariable(name, value);
}
}
}
+75
View File
@@ -0,0 +1,75 @@
using System.ComponentModel;
using System.Text;
namespace MAF1.Tools;
public static class FileTools
{
[Description("Read the full UTF-8 text of a local file. Always call this before judging city names.")]
public static string ReadTextFile([Description("Absolute or relative path of the file to read.")] string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return "未提供文件路径。";
}
string? resolved = Resolve(path.Trim().Trim('"'));
if (resolved is null)
{
return $"找不到文件:{path}";
}
FileInfo info = new(resolved);
if (info.Length > 64 * 1024)
{
return $"文件过大({info.Length} 字节),请换一个不超过 64KB 的文本文件。";
}
return File.ReadAllText(resolved, Encoding.UTF8);
}
private static string? Resolve(string path)
{
if (Path.IsPathRooted(path) && File.Exists(path))
{
return Path.GetFullPath(path);
}
string fromCwd = Path.GetFullPath(path);
if (File.Exists(fromCwd))
{
return fromCwd;
}
foreach (string root in SearchRoots())
{
string candidate = Path.GetFullPath(Path.Combine(root, path));
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
private static IEnumerable<string> SearchRoots()
{
yield return AppContext.BaseDirectory;
string? contentRoot = Environment.GetEnvironmentVariable("MAF1_CONTENT_ROOT");
if (!string.IsNullOrWhiteSpace(contentRoot))
{
yield return contentRoot;
}
DirectoryInfo? parent = Directory.GetParent(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar));
if (parent is not null)
{
yield return parent.FullName;
if (parent.Parent is not null)
{
yield return parent.Parent.FullName;
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace MAF1.Tools;
public sealed class WeatherOptions
{
public string Provider { get; set; } = "Wttr";
public string Language { get; set; } = "zh";
public WttrOptions Wttr { get; set; } = new();
public OpenWeatherOptions OpenWeather { get; set; } = new();
}
public sealed class WttrOptions
{
public string UrlTemplate { get; set; } = "https://wttr.in/{location}?lang={lang}&format=3";
}
public sealed class OpenWeatherOptions
{
public string UrlTemplate { get; set; } =
"https://api.openweathermap.org/data/2.5/weather?q={location}&appid={apiKey}&units=metric&lang={lang}";
public string ApiKey { get; set; } = "";
}
+100
View File
@@ -0,0 +1,100 @@
using System.ComponentModel;
using System.Net.Http.Headers;
using System.Text.Json;
namespace MAF1.Tools;
public sealed class WeatherTools(WeatherOptions options, HttpClient http)
{
[Description("Look up live weather for a city or location. Always call this instead of guessing.")]
public async Task<string> GetWeather(
[Description("City or location name, for example Amsterdam or 北京.")] string location)
{
if (string.IsNullOrWhiteSpace(location))
{
return "未提供地点,无法查询天气。";
}
string provider = options.Provider.Trim();
try
{
if (provider.Equals("OpenWeather", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(options.OpenWeather.ApiKey))
{
return "OpenWeather 已启用,但 appsettings.json 里 Weather:OpenWeather:ApiKey 为空。请填入密钥,或把 Provider 改回 Wttr。";
}
return await QueryOpenWeatherAsync(location);
}
return await QueryWttrAsync(location);
}
catch (Exception ex)
{
return $"查询天气失败({provider} / {location}):{ex.Message}";
}
}
private async Task<string> QueryWttrAsync(string location)
{
string url = Expand(options.Wttr.UrlTemplate, location, apiKey: null);
using HttpResponseMessage response = await http.GetAsync(url);
string body = (await response.Content.ReadAsStringAsync()).Trim();
if (!response.IsSuccessStatusCode)
{
return $"wttr.in 返回 {(int)response.StatusCode}{body}";
}
return string.IsNullOrWhiteSpace(body)
? $"wttr.in 没有返回 {location} 的天气。"
: body;
}
private async Task<string> QueryOpenWeatherAsync(string location)
{
string url = Expand(options.OpenWeather.UrlTemplate, location, options.OpenWeather.ApiKey);
using HttpResponseMessage response = await http.GetAsync(url);
string body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
return $"OpenWeather 返回 {(int)response.StatusCode}{body}";
}
using JsonDocument doc = JsonDocument.Parse(body);
JsonElement root = doc.RootElement;
string name = root.TryGetProperty("name", out JsonElement nameEl) ? nameEl.GetString() ?? location : location;
string description = root.TryGetProperty("weather", out JsonElement weather)
&& weather.ValueKind == JsonValueKind.Array
&& weather.GetArrayLength() > 0
&& weather[0].TryGetProperty("description", out JsonElement descEl)
? descEl.GetString() ?? ""
: "";
double temp = root.TryGetProperty("main", out JsonElement main) && main.TryGetProperty("temp", out JsonElement tempEl)
? tempEl.GetDouble()
: double.NaN;
int humidity = main.ValueKind == JsonValueKind.Object && main.TryGetProperty("humidity", out JsonElement humidityEl)
? humidityEl.GetInt32()
: 0;
return double.IsNaN(temp)
? body
: $"{name}{description},气温 {temp:0.#}°C,湿度 {humidity}%";
}
private string Expand(string template, string location, string? apiKey)
{
return template
.Replace("{location}", Uri.EscapeDataString(location), StringComparison.OrdinalIgnoreCase)
.Replace("{lang}", Uri.EscapeDataString(options.Language), StringComparison.OrdinalIgnoreCase)
.Replace("{apiKey}", apiKey ?? "", StringComparison.OrdinalIgnoreCase);
}
public static HttpClient CreateHttpClient()
{
HttpClient http = new() { Timeout = TimeSpan.FromSeconds(20) };
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MAF1", "1.0"));
http.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN,zh;q=0.9,en;q=0.8");
return http;
}
}
+113
View File
@@ -0,0 +1,113 @@
using System.ClientModel;
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI;
using OpenAI.Chat;
namespace MAF1.Utils;
public sealed class AgentFactory
{
private readonly ChatClient _chatClient;
public AgentFactory(LlmOptions options)
{
if (string.IsNullOrWhiteSpace(options.ApiKey))
{
throw new InvalidOperationException(
"""
appsettings.json Llm
OPENAI_API_KEY / Llm:ApiKey
OPENAI_ENDPOINT / Llm:Endpoint
OPENAI_CHAT_MODEL / Llm:Model
""");
}
string model = string.IsNullOrWhiteSpace(options.Model)
? DefaultModelFor(options.Endpoint)
: options.Model;
if (LooksLikeAzureOpenAI(options.Endpoint))
{
Console.Error.WriteLine($"使用 Azure OpenAI: {options.Endpoint} 部署: {model}");
_chatClient = new AzureOpenAIClient(new Uri(options.Endpoint), new ApiKeyCredential(options.ApiKey))
.GetChatClient(model);
return;
}
OpenAIClientOptions clientOptions = new();
if (!string.IsNullOrWhiteSpace(options.Endpoint))
{
clientOptions.Endpoint = ToOpenAICompatibleEndpoint(options.Endpoint);
}
Console.Error.WriteLine($"使用 OpenAI 兼容接口: {clientOptions.Endpoint?.ToString() ?? "https://api.openai.com/v1"} 模型: {model}");
_chatClient = new OpenAIClient(new ApiKeyCredential(options.ApiKey), clientOptions)
.GetChatClient(model);
}
public AIAgent CreateAgent(string name, string instructions, IList<AITool>? tools = null)
{
return _chatClient.AsAIAgent(instructions: instructions, name: name, tools: tools);
}
public static LlmOptions Load(IConfiguration config)
{
LlmOptions options = config.GetSection("Llm").Get<LlmOptions>() ?? new LlmOptions();
options.ApiKey = FirstNonEmpty(options.ApiKey, Env("OPENAI_API_KEY"), Env("AZURE_OPENAI_API_KEY"));
options.Endpoint = FirstNonEmpty(options.Endpoint, Env("OPENAI_ENDPOINT"), Env("OPENAI_BASE_URL"), Env("AZURE_OPENAI_ENDPOINT"));
options.Model = FirstNonEmpty(options.Model, Env("OPENAI_CHAT_MODEL"), Env("AZURE_OPENAI_DEPLOYMENT_NAME"));
return options;
}
private static string DefaultModelFor(string? endpoint)
{
if (!string.IsNullOrWhiteSpace(endpoint) && endpoint.Contains("deepseek", StringComparison.OrdinalIgnoreCase))
{
return "deepseek-chat";
}
return "gpt-4o-mini";
}
private static bool LooksLikeAzureOpenAI(string? endpoint)
{
if (string.IsNullOrWhiteSpace(endpoint) || !Uri.TryCreate(endpoint, UriKind.Absolute, out Uri? uri))
{
return false;
}
string host = uri.Host;
return host.Contains("openai.azure.com", StringComparison.OrdinalIgnoreCase)
|| host.Contains("cognitiveservices.azure.com", StringComparison.OrdinalIgnoreCase)
|| host.Contains("services.ai.azure.com", StringComparison.OrdinalIgnoreCase);
}
private static Uri ToOpenAICompatibleEndpoint(string endpoint)
{
string trimmed = endpoint.TrimEnd('/');
if (!trimmed.EndsWith("/v1", StringComparison.OrdinalIgnoreCase))
{
trimmed += "/v1";
}
return new Uri(trimmed);
}
private static string? Env(string name) => Environment.GetEnvironmentVariable(name);
private static string FirstNonEmpty(params string?[] values)
{
foreach (string? value in values)
{
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return "";
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace MAF1.Utils;
public static class JsonText
{
public static string UnwrapObject(string text)
{
string trimmed = text.Trim();
if (trimmed.StartsWith("```", StringComparison.Ordinal))
{
int firstNewLine = trimmed.IndexOf('\n');
int lastFence = trimmed.LastIndexOf("```", StringComparison.Ordinal);
if (firstNewLine >= 0 && lastFence > firstNewLine)
{
trimmed = trimmed[(firstNewLine + 1)..lastFence].Trim();
}
}
int objectStart = trimmed.IndexOf('{');
int objectEnd = trimmed.LastIndexOf('}');
if (objectStart >= 0 && objectEnd > objectStart)
{
return trimmed[objectStart..(objectEnd + 1)];
}
return trimmed;
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace MAF1.Utils;
public sealed class LlmOptions
{
public string ApiKey { get; set; } = "";
public string Endpoint { get; set; } = "";
public string Model { get; set; } = "";
}
+55
View File
@@ -0,0 +1,55 @@
using System.Runtime.InteropServices;
using System.Text;
namespace MAF1.Utils;
public static class WindowsConsole
{
private const uint Utf8CodePage = 65001;
private const int StdOutputHandle = -11;
private const uint EnableVirtualTerminalProcessing = 0x0004;
public static void EnableUtf8()
{
if (!OperatingSystem.IsWindows())
{
Console.OutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
Console.InputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
return;
}
SetConsoleOutputCP(Utf8CodePage);
SetConsoleCP(Utf8CodePage);
UTF8Encoding utf8 = new(encoderShouldEmitUTF8Identifier: false);
Console.OutputEncoding = utf8;
Console.InputEncoding = utf8;
nint stdout = GetStdHandle(StdOutputHandle);
if (stdout != nint.Zero && GetConsoleMode(stdout, out uint mode))
{
SetConsoleMode(stdout, mode | EnableVirtualTerminalProcessing);
}
StreamWriter writer = new(Console.OpenStandardOutput(), utf8)
{
AutoFlush = true,
};
Console.SetOut(writer);
}
[DllImport("kernel32.dll")]
private static extern bool SetConsoleOutputCP(uint wCodePageID);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleCP(uint wCodePageID);
[DllImport("kernel32.dll")]
private static extern nint GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll")]
private static extern bool GetConsoleMode(nint hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
}