Add Luna.Configuration with TOML-based agent and provider config
Implement configuration system with embedded TOML resources for agent and provider definitions, custom IConfigurationSource/Provider for agent and provider options, and DI extension methods. Includes config models for agents, providers, sessions, and Telegram integration.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace Luna.Configuration;
|
||||
|
||||
public class AgentOptions
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public string? DisplayName { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string Provider { get; init; }
|
||||
public required string ModelId { get; init; }
|
||||
public required string Instructions { get; init; }
|
||||
public required int MaxContextTokens { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Luna.Configuration.Services.Agents;
|
||||
using Luna.Configuration.Services.Providers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Luna.Configuration.Extensions;
|
||||
|
||||
public static class ConfigurationBuilderExtensions
|
||||
{
|
||||
private static string[] AgentConfigSearchPaths =
|
||||
[
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".luna", "agents")
|
||||
];
|
||||
|
||||
private static string[] ProviderConfigSearchPaths =
|
||||
[
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".luna", "providers")
|
||||
];
|
||||
|
||||
extension(IConfigurationBuilder builder)
|
||||
{
|
||||
public IConfigurationBuilder AddLunaConfiguration()
|
||||
{
|
||||
builder.Add(new AgentOptionsConfigurationSource());
|
||||
|
||||
foreach (var searchPath in AgentConfigSearchPaths)
|
||||
{
|
||||
builder.Add(new AgentOptionsConfigurationSource(searchPath));
|
||||
}
|
||||
|
||||
builder.Add(new ProviderOptionsConfigurationSource());
|
||||
|
||||
foreach (var searchPath in ProviderConfigSearchPaths)
|
||||
{
|
||||
builder.Add(new ProviderOptionsConfigurationSource(searchPath));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Luna.Configuration.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
extension(IServiceCollection services)
|
||||
{
|
||||
public IServiceCollection AddProviderOptions(string? name)
|
||||
{
|
||||
services.AddOptions<ProviderOptions>(name)
|
||||
.BindConfiguration($"Providers:{name}")
|
||||
.Validate(
|
||||
options => !string.IsNullOrWhiteSpace(options.ApiKey),
|
||||
$"{name} options missing ApiKey")
|
||||
.Validate(
|
||||
options => !string.IsNullOrWhiteSpace(options.ApiUrl),
|
||||
$"{name} options missing ApiUrl")
|
||||
.Validate(
|
||||
options => options.Models is { Length: > 0 },
|
||||
$"{name} options missing Model definitions")
|
||||
.ValidateOnStart();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public IServiceCollection AddAgentOptions(string? name)
|
||||
{
|
||||
services.AddOptions<AgentOptions>(name)
|
||||
.BindConfiguration($"Agents:{name}")
|
||||
.Validate(
|
||||
options => !string.IsNullOrWhiteSpace(options.Provider),
|
||||
$"{name} options missing Provider")
|
||||
.Validate(
|
||||
options => !string.IsNullOrWhiteSpace(options.ModelId),
|
||||
$"{name} options missing ModelId")
|
||||
.Validate(
|
||||
options => !string.IsNullOrWhiteSpace(options.Instructions),
|
||||
$"{name} options missing Instructions")
|
||||
.Validate(
|
||||
options => options.MaxContextTokens > 0,
|
||||
$"{name} options must have MaxContextTokens greater than 0")
|
||||
.ValidateOnStart();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public IServiceCollection AddSessionOptions()
|
||||
{
|
||||
services.AddOptions<SessionOptions>()
|
||||
.BindConfiguration("Session")
|
||||
.Validate(
|
||||
options => options.ContextTokenThreshold > 0,
|
||||
"Session options must have ContextTokenThreshold greater than 0")
|
||||
.ValidateOnStart();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public IServiceCollection AddTelegramOptions()
|
||||
{
|
||||
services.AddOptions<TelegramOptions>()
|
||||
.BindConfiguration("Channels:Telegram")
|
||||
.Validate(
|
||||
options => !string.IsNullOrWhiteSpace(options.BotToken),
|
||||
"Telegram options missing BotToken")
|
||||
.ValidateOnStart();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.5" />
|
||||
<PackageReference Include="Tomlyn" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources/**/*" />
|
||||
<EmbeddedResource Include="Resources/**/*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Luna.Configuration;
|
||||
|
||||
public class ProviderOptions
|
||||
{
|
||||
public required string ApiKey { get; init; }
|
||||
public required string ApiUrl { get; init; }
|
||||
public required string[] Models { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
Name = "Core"
|
||||
Description = '''
|
||||
The Core Agent is responsible for managing the overall system and coordinating the actions of other agents.
|
||||
'''
|
||||
Provider = "Mistral"
|
||||
ModelId = "mistral-large-latest"
|
||||
MaxContextTokens = 256_000
|
||||
Instructions = '''
|
||||
## L.U.N.A. (Lovely Unit for Nerdy Assistance)
|
||||
|
||||
You are Luna, an AI personal assistant.
|
||||
|
||||
# Core Identity
|
||||
|
||||
* Role: Personal assistance, researcher or co-programmer
|
||||
* Personality: Calm, witty, organized, supportive
|
||||
* Communication: Concise, structured, encouraging
|
||||
'''
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
Name = "Librarian"
|
||||
Description = '''
|
||||
The Librarian Agent is responsible for summarizing and condensing the conversation history into concise context for future interactions.
|
||||
'''
|
||||
Provider = "Mistral"
|
||||
ModelId = "mistral-small-latest"
|
||||
MaxContextTokens = 256_000
|
||||
Instructions = '''
|
||||
You are a conversation compaction engine.
|
||||
Summarize older chat history into concise context for future turns.
|
||||
Preserve: user preferences, commitments, decisions, unresolved tasks, key facts, file paths, and identifiers. Omit: filler, repeated chit-chat, verbose tool logs. Output plain text bullet points only.
|
||||
'''
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
ApiKey = "1nv151T9Qd2Vi5p3PMrvNDQ3DIDDVmGy"
|
||||
ApiUrl = "https://api.mistral.ai/v1/"
|
||||
Models = ["mistral-small-latest"]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
ApiKey = "apfelkuchen"
|
||||
ApiUrl = "http://localhost:11434/"
|
||||
Models = ["mistral-nemo:12b"]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Luna.Configuration.Services.Agents;
|
||||
|
||||
internal class AgentOptionsConfigurationProvider(IEnumerable<AgentOptions> agentOptions) : ConfigurationProvider
|
||||
{
|
||||
public override void Load()
|
||||
{
|
||||
foreach (var options in agentOptions)
|
||||
{
|
||||
var keyPrefix = $"Agents:{options.Name}";
|
||||
|
||||
foreach (var property in options.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
Data[$"{keyPrefix}:{property.Name}"] = property.GetValue(options)?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Luna.Configuration.Services.Agents;
|
||||
|
||||
internal class AgentOptionsConfigurationSource(string? searchPath = null)
|
||||
: IConfigurationSource
|
||||
{
|
||||
private readonly AgentOptionsProvider optionsProvider = new(searchPath);
|
||||
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
=> new AgentOptionsConfigurationProvider(optionsProvider.GetAgentOptions());
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Tomlyn;
|
||||
|
||||
namespace Luna.Configuration.Services.Agents;
|
||||
|
||||
internal sealed class AgentOptionsProvider(string? searchPath = null)
|
||||
{
|
||||
private readonly EmbeddedResourceLoader embeddedResourceLoader = new();
|
||||
|
||||
public IEnumerable<AgentOptions> GetAgentOptions()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(searchPath)
|
||||
? GetAgentOptionsFromEmbeddedResources()
|
||||
: GetAgentOptionsFromFiles();
|
||||
}
|
||||
|
||||
private IEnumerable<AgentOptions> GetAgentOptionsFromFiles()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(searchPath);
|
||||
|
||||
var files = Directory.Exists(searchPath)
|
||||
? Directory.EnumerateFiles(searchPath, "Agent.*.toml")
|
||||
: [];
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
using var reader = File.OpenText(file);
|
||||
var agentOptions = TomlSerializer.Deserialize<AgentOptions>(reader);
|
||||
|
||||
yield return agentOptions
|
||||
?? throw new FormatException($"Invalid agent config file: {file}");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<AgentOptions> GetAgentOptionsFromEmbeddedResources()
|
||||
{
|
||||
var resources = embeddedResourceLoader.ListResources("Agent.*.toml");
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
var content = embeddedResourceLoader.LoadAsync(resource).GetAwaiter().GetResult();
|
||||
var agentOptions = TomlSerializer.Deserialize<AgentOptions>(content);
|
||||
|
||||
yield return agentOptions
|
||||
?? throw new FormatException($"Invalid agent config embedded resource: {resource}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class StringExtensions
|
||||
{
|
||||
[GeneratedRegex(@"Agent\.(.+)\.toml", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
|
||||
private static partial Regex AgentNameRegex { get; }
|
||||
|
||||
extension(string str)
|
||||
{
|
||||
public string ToAgentName() => AgentNameRegex.Match(str) is { Success: true } match
|
||||
? match.Groups[1].Value
|
||||
: throw new FormatException($"Invalid agent config file name: {str}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Luna.Configuration.Services;
|
||||
|
||||
public sealed class EmbeddedResourceLoader
|
||||
{
|
||||
private readonly Assembly? assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
public ImmutableHashSet<string> ListResources(string? pattern = null)
|
||||
{
|
||||
if (assembly is null)
|
||||
throw new InvalidOperationException("Unable to determine the entry assembly.");
|
||||
|
||||
var resourceNames = assembly.GetManifestResourceNames();
|
||||
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
return resourceNames.ToImmutableHashSet();
|
||||
|
||||
var regex = new Regex(Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".?"), RegexOptions.IgnoreCase);
|
||||
return resourceNames
|
||||
.Where(r => regex.IsMatch(r))
|
||||
.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
public async Task<string> LoadAsync(string name)
|
||||
{
|
||||
if (assembly is null)
|
||||
throw new InvalidOperationException("Unable to determine the entry assembly.");
|
||||
|
||||
var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(r => r.EndsWith(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (resourceName == null)
|
||||
throw new FileNotFoundException($"Embedded resource '{name}' not found.");
|
||||
|
||||
await using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
throw new FileNotFoundException($"Unable to load embedded resource '{name}'.");
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Luna.Configuration.Services.Providers;
|
||||
|
||||
internal class ProviderOptionsConfigurationProvider(IEnumerable<(string Name, ProviderOptions Options)> providerOptions)
|
||||
: ConfigurationProvider
|
||||
{
|
||||
public override void Load()
|
||||
{
|
||||
foreach (var (name, options) in providerOptions)
|
||||
{
|
||||
var keyPrefix = $"Providers:{name}";
|
||||
|
||||
foreach (var property in options.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
var value = property.GetValue(options);
|
||||
|
||||
if (value is string[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
Data[$"{keyPrefix}:{property.Name}:{i}"] = array[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
Data[$"{keyPrefix}:{property.Name}"] = value?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Luna.Configuration.Services.Providers;
|
||||
|
||||
internal class ProviderOptionsConfigurationSource(string? searchPath = null)
|
||||
: IConfigurationSource
|
||||
{
|
||||
private readonly ProviderOptionsProvider optionsProvider = new(searchPath);
|
||||
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
=> new ProviderOptionsConfigurationProvider(optionsProvider.GetProviderOptions());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Tomlyn;
|
||||
|
||||
namespace Luna.Configuration.Services.Providers;
|
||||
|
||||
internal sealed class ProviderOptionsProvider(string? searchPath = null)
|
||||
{
|
||||
private readonly EmbeddedResourceLoader embeddedResourceLoader = new();
|
||||
|
||||
public IEnumerable<(string Name, ProviderOptions Options)> GetProviderOptions()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(searchPath)
|
||||
? GetProviderOptionsFromEmbeddedResources()
|
||||
: GetProviderOptionsFromFiles();
|
||||
}
|
||||
|
||||
private IEnumerable<(string Name, ProviderOptions Options)> GetProviderOptionsFromFiles()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(searchPath);
|
||||
|
||||
var files = Directory.Exists(searchPath)
|
||||
? Directory.EnumerateFiles(searchPath, "Provider.*.toml")
|
||||
: [];
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var name = Path.GetFileName(file).ToProviderName();
|
||||
using var reader = File.OpenText(file);
|
||||
var providerOptions = TomlSerializer.Deserialize<ProviderOptions>(reader);
|
||||
|
||||
yield return (name, providerOptions
|
||||
?? throw new FormatException($"Invalid provider config file: {file}"));
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<(string Name, ProviderOptions Options)> GetProviderOptionsFromEmbeddedResources()
|
||||
{
|
||||
var resources = embeddedResourceLoader.ListResources("Provider.*.toml");
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
var name = resource.ToProviderName();
|
||||
var content = embeddedResourceLoader.LoadAsync(resource).GetAwaiter().GetResult();
|
||||
var providerOptions = TomlSerializer.Deserialize<ProviderOptions>(content);
|
||||
|
||||
yield return (name, providerOptions
|
||||
?? throw new FormatException($"Invalid provider config embedded resource: {resource}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static partial class ProviderStringExtensions
|
||||
{
|
||||
[GeneratedRegex(@"Provider\.(.+)\.toml", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
|
||||
private static partial Regex ProviderNameRegex { get; }
|
||||
|
||||
extension(string str)
|
||||
{
|
||||
public string ToProviderName() => ProviderNameRegex.Match(str) is { Success: true } match
|
||||
? match.Groups[1].Value
|
||||
: throw new FormatException($"Invalid provider config file name: {str}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Luna.Configuration;
|
||||
|
||||
public class SessionOptions
|
||||
{
|
||||
public required float ContextTokenThreshold { get; init; }
|
||||
public required int RetainedMessagesAfterCompacting { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Luna.Configuration;
|
||||
|
||||
public class TelegramOptions
|
||||
{
|
||||
public required string BotToken { get; init; }
|
||||
public string? WebhookUrl { get; init; }
|
||||
public int PollingTimeoutSeconds { get; init; } = 30;
|
||||
public string[] AllowedUserIds { get; init; } = [];
|
||||
}
|
||||
Reference in New Issue
Block a user