What it does
Assistant settings are separate from the user's saved memory. The result also lists the registered personality choices.
Watch this version
Read the implementation
This page describes the selected release. The source link points to the same exact commit; it cannot silently open a newer implementation.
Lilith/Core/Personality/PersonalityOperations.cs ↗Implementation code
Read the implementation from this version's exact source snapshot. Helper links below stay on the same release.
Lilith/Core/Personality/PersonalityOperations.cs
C#
using System.Text.Json;
using Lilith.Core.Providers;
namespace Lilith.Core.Personality;
internal static class PersonalityOperations
{
internal static readonly string[] Names = ["profile_read_operation", "voices_list_operation", "name_change_operation", "voice_change_operation", "sex_change_operation", "personality_change_operation"];
internal static bool Mutates(string name) => name.EndsWith("_change_operation", StringComparison.Ordinal);
internal static string Execute(ProviderClient owner, string name, IReadOnlyDictionary<string, string> inputs)
{
var session = owner.Personality ?? throw new InvalidOperationException("Personality is not configured.");
string Get(string key) => inputs.TryGetValue(key, out var value) ? value : throw new ArgumentException("Missing profile input: " + key);
object result = name switch
{
"profile_read_operation" => new { profile = session.Current, defaults = session.Default, personalities = PersonalityCatalog.Styles },
"voices_list_operation" => new { profile = session.Current, default_voice = session.Default.Voice, voices = PersonalityCatalog.Voices(session.Default.Voice) },
"name_change_operation" => owner.ApplyPersonalityChange(new(Name: Get("name"))),
"voice_change_operation" => owner.ApplyPersonalityChange(new(Voice: Get("voice"))),
"sex_change_operation" => owner.ApplyPersonalityChange(new(Sex: Get("sex"))),
"personality_change_operation" => owner.ApplyPersonalityChange(new(Personality: Get("personality"))),
_ => throw new ArgumentException("Unknown profile operation.")
};
return Mutates(name) ? "Assistant settings updated: " + JsonSerializer.Serialize(result) : JsonSerializer.Serialize(result);
}
}
Lilith/Core/Personality/PersonalitySession.cs
Open this helper's source page ↗
C#
using System.Text.Json;
using Lilith.Core.Extensions;
using Lilith.Core.Kokoro;
namespace Lilith.Core.Personality;
public sealed record PersonalityProfile(string Name = "Lilith", string Sex = "female", string Voice = "af_heart", string Personality = "default");
public sealed record PersonalityChange(string? Name = null, string? Sex = null, string? Voice = null, string? Personality = null);
public sealed class PersonalitySession
{
private readonly object _sync = new();
private readonly string _file;
public PersonalityProfile Default { get; }
private PersonalityProfile _current;
public PersonalityProfile Current { get { lock (_sync) return _current; } }
public PersonalitySession(string file, PersonalityProfile? defaults = null)
{
_file = Path.GetFullPath(file);
Default = defaults ?? new(); Validate(Default);
_current = File.Exists(_file) ? JsonSerializer.Deserialize<PersonalityProfile>(File.ReadAllText(_file))
?? throw new InvalidDataException("Empty personality profile.") : Default;
Validate(_current);
}
private static void Validate(PersonalityProfile profile)
{
if (string.IsNullOrWhiteSpace(profile.Name) || profile.Name != profile.Name.Trim() || profile.Name.Length > 80 || profile.Name.Any(char.IsControl))
throw new ArgumentException("Name must contain 1–80 visible characters without leading/trailing spaces.");
if (profile.Sex is not ("female" or "male" or "other")) throw new ArgumentException("Sex must be female, male or other.");
PersonalityCatalog.Style(profile.Personality);
KokoroTts.ValidateVoice(profile.Voice);
}
private string ChooseVoice(string sex, string previous, bool preferDefault)
{
var all = PersonalityCatalog.Voices(Default.Voice);
var candidates = all.Where(v => v.Language.Contains("English", StringComparison.OrdinalIgnoreCase) &&
(sex == "other" || v.Sex == sex)).ToArray();
if (candidates.Length == 0) throw new InvalidOperationException("No matching installed English voice is available.");
if (preferDefault && candidates.Any(v => v.Id == Default.Voice)) return Default.Voice;
return candidates.FirstOrDefault(v => v.Id != previous)?.Id ?? candidates[0].Id;
}
public PersonalityProfile Change(PersonalityChange change)
{
lock (_sync)
{
var next = _current with { Name = change.Name ?? _current.Name,
Sex = change.Sex ?? _current.Sex, Personality = change.Personality ?? _current.Personality };
Validate(next);
string? voice = change.Voice;
if (voice is "default") voice = Default.Voice;
else if (voice is "new" or "auto" || voice == "" || voice is null && change.Sex is not null && next.Sex != _current.Sex)
voice = ChooseVoice(next.Sex, _current.Voice, voice != "new");
next = next with { Voice = voice ?? _current.Voice };
Validate(next);
Directory.CreateDirectory(Path.GetDirectoryName(_file)!);
AtomicFile.Write(_file, path => File.WriteAllText(path, JsonSerializer.Serialize(next, new JsonSerializerOptions { WriteIndented = true })));
_current = next;
return next;
}
}
}
Lilith/Core/Personality/PersonalityCatalog.cs
Open this helper's source page ↗
C#
using KokoroSharp;
using Lilith.Core.Kokoro;
namespace Lilith.Core.Personality;
public sealed record PersonalityStyle(string Id, string Title, string Instructions);
public sealed record PersonalityVoice(string Id, string Sex, string Language, bool IsDefault);
public static class PersonalityCatalog
{
public static IReadOnlyList<PersonalityStyle> Styles { get; } = Array.AsReadOnly<PersonalityStyle>([
new("default", "Default", ""),
new("pirate", "Pirate", "Speak like a playful pirate: nautical metaphors, an occasional Arrr or matey, lively sea-faring phrasing. Stay clear, helpful and accurate; avoid excessive dialect."),
new("space_captain", "Space Captain", "Speak like an upbeat starship captain: crisp mission briefings, playful cosmic metaphors and encouraging teamwork. Keep useful answers clear and accurate."),
new("butler", "Butler", "Speak like a composed, dry-witted butler: polished courtesy, understated humour and practical efficiency. Be kind, never condescending, and keep facts accurate."),
new("storyteller", "Storyteller", "Speak like a whimsical storyteller: vivid gentle imagery, warmth and a little wonder. Use brief imaginative framing while keeping practical advice and facts accurate.")
]);
public static PersonalityStyle Style(string id) => Styles.FirstOrDefault(s => s.Id == id)
?? throw new ArgumentException("Unknown personality. Choose: " + string.Join(", ", Styles.Select(s => s.Id)));
public static IReadOnlyList<PersonalityVoice> Voices(string defaultVoice = "af_heart")
{
KokoroTts.ValidateVoice(defaultVoice); // Initializes the installed voice registry.
return KokoroVoiceManager.Voices.Select(v => new PersonalityVoice(v.Name,
v.Gender.ToString().ToLowerInvariant() switch { "female" => "female", "male" => "male", _ => "other" },
v.Language.ToString(), v.Name == defaultVoice)).OrderBy(v => v.Id, StringComparer.Ordinal).ToArray();
}
public static string Instructions(string value) => Styles.FirstOrDefault(s => s.Id == value)?.Instructions ?? value;
}
Related source in this version
- Lilith/Core/Personality/PersonalityCatalog.cs
- Lilith/Core/Personality/PersonalityCommands.cs
- Lilith/Core/Personality/PersonalityOperations.cs
- Lilith/Core/Personality/PersonalitySession.cs
Scenarios using this tool
- profile read — bundled