Ship persona shelves, Exact controls, and overlay author pipeline.
Add Leonid as a shelf-based example with preference_bias slider; support /persona new clone-to-overlay and UI-only delete. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+448
-86
@@ -265,6 +265,13 @@ public sealed class AssistentConfig
|
||||
Scan(_bundledRoot, "bundled");
|
||||
Scan(_overlayRoot, "overlay");
|
||||
|
||||
// Normalize source: overlay-only vs bundled vs both.
|
||||
foreach (string id in byId.Keys.ToList())
|
||||
{
|
||||
var cur = byId[id];
|
||||
byId[id] = (cur.title, cur.accent, PersonaSource(id));
|
||||
}
|
||||
|
||||
// Legacy personas.json titles
|
||||
string overlayJson = Path.Combine(_overlayRoot, "personas.json");
|
||||
JObject legacy = TryReadJson(overlayJson);
|
||||
@@ -283,7 +290,7 @@ public sealed class AssistentConfig
|
||||
}
|
||||
if (byId.TryGetValue(id, out var cur))
|
||||
{
|
||||
byId[id] = (po["title"]?.ToString() ?? cur.title, cur.accent, "overlay+bundled");
|
||||
byId[id] = (po["title"]?.ToString() ?? cur.title, cur.accent, cur.source);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -316,7 +323,7 @@ public sealed class AssistentConfig
|
||||
return def;
|
||||
}
|
||||
|
||||
/// <summary>Exact KV for the system prompt: params tables only (facts stay short / in RAG).</summary>
|
||||
/// <summary>Exact KV for the system prompt: params tables + persona controls (facts stay short / in RAG).</summary>
|
||||
public JObject LoadExactForPrompt(string personaId)
|
||||
{
|
||||
JObject full = LoadExact(personaId);
|
||||
@@ -325,7 +332,7 @@ public sealed class AssistentConfig
|
||||
return full ?? new JObject();
|
||||
}
|
||||
JObject slim = new();
|
||||
foreach (string key in new[] { "generation", "profiles", "aspect_table" })
|
||||
foreach (string key in new[] { "generation", "profiles", "aspect_table", "controls" })
|
||||
{
|
||||
if (full[key] is not null)
|
||||
{
|
||||
@@ -356,6 +363,351 @@ public sealed class AssistentConfig
|
||||
return slim;
|
||||
}
|
||||
|
||||
static readonly HashSet<string> ReservedConfigFiles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"exact.json", "controls.json", "skills.json", "ui.json", "assistant.json",
|
||||
};
|
||||
|
||||
static readonly HashSet<string> ReservedConfigDirs = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"memory-seed", "packs", "skills", "core", "models",
|
||||
};
|
||||
|
||||
/// <summary>Identity shelf filenames (*.json) discovered under layer roots, excluding reserved config.</summary>
|
||||
public List<string> DiscoverIdentityShelfFiles(string personaId)
|
||||
{
|
||||
HashSet<string> names = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string root in LayerRoots(personaId))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (string file in Directory.GetFiles(root, "*.json"))
|
||||
{
|
||||
string name = Path.GetFileName(file);
|
||||
if (ReservedConfigFiles.Contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
// Stable order: persona first, then alpha.
|
||||
return names.OrderBy(n => string.Equals(n, "persona.json", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
|
||||
.ThenBy(n => n, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public JObject LoadControlsSchema(string personaId) => MergeJsonLayers("controls.json", LayerRoots(personaId));
|
||||
|
||||
public JObject LoadControlValues(string personaId)
|
||||
{
|
||||
JObject exact = LoadExact(personaId);
|
||||
return exact["controls"] as JObject ?? new JObject();
|
||||
}
|
||||
|
||||
/// <summary>Clamp and merge control values into overlay exact.json (controls key only).</summary>
|
||||
public JObject SaveControlValues(string personaId, JObject values)
|
||||
{
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
JObject schema = LoadControlsSchema(id);
|
||||
JObject clamped = ClampControls(schema, values ?? new JObject());
|
||||
string dir = Path.Combine(_overlayRoot, "personas", id);
|
||||
Directory.CreateDirectory(dir);
|
||||
string path = Path.Combine(dir, "exact.json");
|
||||
JObject existing = TryReadJson(path) ?? new JObject();
|
||||
existing["controls"] = clamped;
|
||||
File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
return LoadControlValues(id);
|
||||
}
|
||||
|
||||
public static JObject ClampControls(JObject schema, JObject values)
|
||||
{
|
||||
JObject result = new();
|
||||
if (schema is null || values is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
foreach (JProperty prop in schema.Properties())
|
||||
{
|
||||
if (values[prop.Name] is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
JObject def = prop.Value as JObject;
|
||||
if (def is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string type = def["type"]?.ToString() ?? "slider";
|
||||
if (!string.Equals(type, "slider", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!double.TryParse(values[prop.Name]?.ToString(), System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out double v))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double min = def["min"]?.Value<double?>() ?? -1;
|
||||
double max = def["max"]?.Value<double?>() ?? 1;
|
||||
if (v < min)
|
||||
{
|
||||
v = min;
|
||||
}
|
||||
if (v > max)
|
||||
{
|
||||
v = max;
|
||||
}
|
||||
result[prop.Name] = Math.Round(v, 4);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public string RenderControlsBlock(string personaId)
|
||||
{
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
JObject schema = LoadControlsSchema(id);
|
||||
if (schema is null || !schema.Properties().Any())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
JObject values = LoadControlValues(id);
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("### Controls (Exact — current values; user/UI/model may change)");
|
||||
foreach (JProperty prop in schema.Properties())
|
||||
{
|
||||
if (prop.Value is not JObject def)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string type = def["type"]?.ToString() ?? "slider";
|
||||
if (!string.Equals(type, "slider", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double min = def["min"]?.Value<double?>() ?? -1;
|
||||
double max = def["max"]?.Value<double?>() ?? 1;
|
||||
double defVal = def["default"]?.Value<double?>() ?? 0;
|
||||
double cur = values[prop.Name]?.Value<double?>() ?? defVal;
|
||||
string label = def["label"]?.ToString() ?? prop.Name;
|
||||
string hint = def["hint"]?.ToString() ?? "";
|
||||
sb.AppendLine($"- **{prop.Name}** ({label}): {cur.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)} (range {min}…{max})");
|
||||
if (!string.IsNullOrWhiteSpace(hint))
|
||||
{
|
||||
sb.AppendLine($" - {hint}");
|
||||
}
|
||||
if (def["meaning"] is JObject meaning)
|
||||
{
|
||||
foreach (JProperty m in meaning.Properties())
|
||||
{
|
||||
sb.AppendLine($" - {m.Name}: {m.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
public bool IsBundledPersona(string personaId)
|
||||
{
|
||||
string id = SafeId(personaId);
|
||||
if (id is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
string path = Path.Combine(_bundledRoot, "personas", id, "persona.json");
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
public bool IsOverlayPersona(string personaId)
|
||||
{
|
||||
string id = SafeId(personaId);
|
||||
if (id is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
string dir = Path.Combine(_overlayRoot, "personas", id);
|
||||
return Directory.Exists(dir)
|
||||
&& (File.Exists(Path.Combine(dir, "persona.json")) || File.Exists(Path.Combine(dir, "extra.md")));
|
||||
}
|
||||
|
||||
public string PersonaSource(string personaId)
|
||||
{
|
||||
if (IsOverlayPersona(personaId) && !IsBundledPersona(personaId))
|
||||
{
|
||||
return "overlay";
|
||||
}
|
||||
if (IsOverlayPersona(personaId) && IsBundledPersona(personaId))
|
||||
{
|
||||
return "overlay+bundled";
|
||||
}
|
||||
if (IsBundledPersona(personaId))
|
||||
{
|
||||
return "bundled";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static readonly HashSet<string> WritableShelfNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"persona.json", "bio.json", "voice.json", "humor.json", "craft.json",
|
||||
"appearance.json", "outfits.json", "roleplay.json", "likes.json", "dislikes.json",
|
||||
"rules.json", "controls.json", "exact.json", "extra.md",
|
||||
};
|
||||
|
||||
public static bool IsWritableShelfName(string fileName)
|
||||
{
|
||||
string name = Path.GetFileName(fileName ?? "");
|
||||
if (string.IsNullOrWhiteSpace(name) || name.Contains("..") || name.Contains('/') || name.Contains('\\'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (WritableShelfNames.Contains(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Allow extra identity *.json shelves (not reserved).
|
||||
return name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|
||||
&& !ReservedConfigFiles.Contains(name)
|
||||
&& !string.Equals(name, "skills.json", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Materialize merged identity shelves (+ controls/exact.controls) into overlay personas/toId.</summary>
|
||||
public JObject ClonePersonaToOverlay(string fromId, string toId, string title, bool overwrite = false)
|
||||
{
|
||||
string from = SafeId(fromId) ?? throw new ArgumentException("invalid from id");
|
||||
string to = SafeId(toId) ?? throw new ArgumentException("invalid to id");
|
||||
if (IsBundledPersona(to))
|
||||
{
|
||||
throw new InvalidOperationException($"cannot overwrite bundled persona '{to}'");
|
||||
}
|
||||
string dest = Path.Combine(_overlayRoot, "personas", to);
|
||||
if (Directory.Exists(dest) && Directory.EnumerateFileSystemEntries(dest).Any() && !overwrite)
|
||||
{
|
||||
throw new InvalidOperationException($"overlay persona '{to}' already exists");
|
||||
}
|
||||
Directory.CreateDirectory(dest);
|
||||
JObject shelves = LoadIdentityParts(from);
|
||||
foreach (JProperty prop in shelves.Properties())
|
||||
{
|
||||
if (prop.Name == "extra")
|
||||
{
|
||||
string extra = prop.Value?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(extra))
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, "extra.md"), extra.TrimEnd() + "\n", Encoding.UTF8);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (prop.Value is JObject jo && jo.Properties().Any())
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, prop.Name + ".json"),
|
||||
jo.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
JObject controls = LoadControlsSchema(from);
|
||||
if (controls.Properties().Any())
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, "controls.json"),
|
||||
controls.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
JObject values = LoadControlValues(from);
|
||||
if (values.Properties().Any())
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, "exact.json"),
|
||||
new JObject { ["controls"] = values }.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
// Ensure persona.json title.
|
||||
string personaPath = Path.Combine(dest, "persona.json");
|
||||
JObject meta = TryReadJson(personaPath) ?? new JObject();
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
meta["title"] = title.Trim();
|
||||
}
|
||||
meta.Remove("extends");
|
||||
File.WriteAllText(personaPath, meta.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
return new JObject { ["id"] = to, ["title"] = meta["title"]?.ToString() ?? to, ["source"] = "overlay" };
|
||||
}
|
||||
|
||||
/// <summary>Sparse DeepMerge shelves into overlay personas/id. Overlay-only ids (or existing overlay).</summary>
|
||||
public JObject SavePersonaShelves(string personaId, JObject shelves, bool allowBundledShadow = false)
|
||||
{
|
||||
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
|
||||
if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
|
||||
{
|
||||
// v1: do not shadow-write bundled; require clone to a new overlay id.
|
||||
throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
|
||||
}
|
||||
if (IsBundledPersona(id) && !IsOverlayPersona(id))
|
||||
{
|
||||
throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
|
||||
}
|
||||
string dest = Path.Combine(_overlayRoot, "personas", id);
|
||||
Directory.CreateDirectory(dest);
|
||||
if (shelves is null)
|
||||
{
|
||||
return LoadIdentityParts(id);
|
||||
}
|
||||
foreach (JProperty prop in shelves.Properties())
|
||||
{
|
||||
string fileName = prop.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|
||||
|| prop.Name.EndsWith(".md", StringComparison.OrdinalIgnoreCase)
|
||||
? Path.GetFileName(prop.Name)
|
||||
: prop.Name + ".json";
|
||||
if (!IsWritableShelfName(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string path = Path.Combine(dest, fileName);
|
||||
if (fileName.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string text = prop.Value?.Type == JTokenType.String ? prop.Value.ToString() : prop.Value?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
File.WriteAllText(path, text.TrimEnd() + "\n", Encoding.UTF8);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
JObject incoming = prop.Value as JObject;
|
||||
if (incoming is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
JObject existing = TryReadJson(path) ?? new JObject();
|
||||
JObject merged = DeepMerge(existing, incoming);
|
||||
File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
return LoadIdentityParts(id);
|
||||
}
|
||||
|
||||
public bool DeleteOverlayPersona(string personaId)
|
||||
{
|
||||
string id = SafeId(personaId);
|
||||
if (id is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (IsBundledPersona(id) && !IsOverlayPersona(id))
|
||||
{
|
||||
throw new InvalidOperationException($"cannot delete bundled persona '{id}'");
|
||||
}
|
||||
// Only delete overlay folder; never touch bundled.
|
||||
string dest = Path.Combine(_overlayRoot, "personas", id);
|
||||
if (!Directory.Exists(dest))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (IsBundledPersona(id))
|
||||
{
|
||||
// Overlay shadow of a bundled id — remove overlay only (reverts to bundled).
|
||||
Directory.Delete(dest, true);
|
||||
return true;
|
||||
}
|
||||
Directory.Delete(dest, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public JObject LoadAssistant(string personaId) => MergeJsonLayers("assistant.json", LayerRoots(personaId));
|
||||
|
||||
public JObject LoadUi(string personaId) => MergeJsonLayers("ui.json", LayerRoots(personaId));
|
||||
@@ -492,16 +844,25 @@ public sealed class AssistentConfig
|
||||
public JObject LoadIdentityParts(string personaId)
|
||||
{
|
||||
var roots = LayerRoots(personaId).ToList();
|
||||
JObject persona = MergeJsonLayers("persona.json", roots);
|
||||
JObject voice = MergeJsonLayers("voice.json", roots);
|
||||
JObject likes = MergeJsonLayers("likes.json", roots);
|
||||
JObject dislikes = MergeJsonLayers("dislikes.json", roots);
|
||||
JObject rules = MergeJsonLayers("rules.json", roots);
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
JObject shelves = new();
|
||||
foreach (string fileName in DiscoverIdentityShelfFiles(id))
|
||||
{
|
||||
string key = Path.GetFileNameWithoutExtension(fileName);
|
||||
JObject merged = MergeJsonLayers(fileName, roots);
|
||||
if (merged is not null && merged.Properties().Any())
|
||||
{
|
||||
shelves[key] = merged;
|
||||
}
|
||||
}
|
||||
// Always expose persona key (may be empty template).
|
||||
if (shelves["persona"] is null)
|
||||
{
|
||||
shelves["persona"] = MergeJsonLayers("persona.json", roots);
|
||||
}
|
||||
string extra = MergeTextLayers("extra.md", roots);
|
||||
|
||||
// Legacy personas.json: only when no overlay persona folder exists for this id
|
||||
// (gpu-rent now seeds personas/<id>/extra.md instead of dumping personas.json).
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
// Legacy personas.json: only when no overlay persona folder exists for this id.
|
||||
string overlayPersonaDir = Path.Combine(_overlayRoot, "personas", id);
|
||||
bool hasOverlayFolder = Directory.Exists(overlayPersonaDir)
|
||||
&& (File.Exists(Path.Combine(overlayPersonaDir, "persona.json"))
|
||||
@@ -516,11 +877,13 @@ public sealed class AssistentConfig
|
||||
{
|
||||
if (t is JObject po && string.Equals(SafeId(po["id"]?.ToString()), id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
JObject persona = shelves["persona"] as JObject ?? new JObject();
|
||||
string title = po["title"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
persona["title"] = title;
|
||||
}
|
||||
shelves["persona"] = persona;
|
||||
string prompt = po["prompt"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
@@ -532,44 +895,71 @@ public sealed class AssistentConfig
|
||||
}
|
||||
}
|
||||
|
||||
return new JObject
|
||||
{
|
||||
["persona"] = persona,
|
||||
["voice"] = voice,
|
||||
["likes"] = likes,
|
||||
["dislikes"] = dislikes,
|
||||
["rules"] = rules,
|
||||
["extra"] = extra ?? "",
|
||||
};
|
||||
shelves["extra"] = extra ?? "";
|
||||
return shelves;
|
||||
}
|
||||
|
||||
static string FormatCategoryMap(JObject obj)
|
||||
static void AppendTokenMarkdown(StringBuilder sb, JToken token, int depth)
|
||||
{
|
||||
if (obj is null || !obj.Properties().Any())
|
||||
if (token is null || token.Type == JTokenType.Null)
|
||||
{
|
||||
return "";
|
||||
return;
|
||||
}
|
||||
List<string> parts = [];
|
||||
foreach (JProperty prop in obj.Properties())
|
||||
string indent = new string(' ', Math.Max(0, depth) * 2);
|
||||
if (token is JArray arr)
|
||||
{
|
||||
if (prop.Value is JArray arr)
|
||||
foreach (JToken item in arr)
|
||||
{
|
||||
string joined = string.Join(", ", arr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
if (!string.IsNullOrWhiteSpace(joined))
|
||||
if (item is JObject || item is JArray)
|
||||
{
|
||||
parts.Add($"{prop.Name}: {joined}");
|
||||
sb.AppendLine($"{indent}-");
|
||||
AppendTokenMarkdown(sb, item, depth + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
string s = item?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
sb.AppendLine($"{indent}- {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (prop.Value?.Type == JTokenType.String)
|
||||
return;
|
||||
}
|
||||
if (token is JObject obj)
|
||||
{
|
||||
foreach (JProperty prop in obj.Properties())
|
||||
{
|
||||
string s = prop.Value.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
if (prop.Value is JArray or JObject)
|
||||
{
|
||||
parts.Add($"{prop.Name}: {s}");
|
||||
sb.AppendLine($"{indent}- **{prop.Name}:**");
|
||||
AppendTokenMarkdown(sb, prop.Value, depth + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
string s = prop.Value?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
sb.AppendLine($"{indent}- **{prop.Name}:** {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
return string.Join("; ", parts);
|
||||
string scalar = token.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(scalar))
|
||||
{
|
||||
sb.AppendLine($"{indent}- {scalar}");
|
||||
}
|
||||
}
|
||||
|
||||
static string TitleCaseShelf(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return key;
|
||||
}
|
||||
return char.ToUpperInvariant(key[0]) + key[1..];
|
||||
}
|
||||
|
||||
public string RenderIdentityBlock(string personaId)
|
||||
@@ -577,74 +967,41 @@ public sealed class AssistentConfig
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
JObject parts = LoadIdentityParts(id);
|
||||
JObject persona = parts["persona"] as JObject ?? new JObject();
|
||||
JObject voice = parts["voice"] as JObject ?? new JObject();
|
||||
JObject likes = parts["likes"] as JObject ?? new JObject();
|
||||
JObject dislikes = parts["dislikes"] as JObject ?? new JObject();
|
||||
JObject rules = parts["rules"] as JObject ?? new JObject();
|
||||
string extra = parts["extra"]?.ToString() ?? "";
|
||||
|
||||
string title = persona["title"]?.ToString() ?? id;
|
||||
string tagline = persona["tagline"]?.ToString();
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine($"## Persona: {id} — {title}");
|
||||
|
||||
List<string> voiceBits = [];
|
||||
if (voice["verbosity"] != null)
|
||||
if (!string.IsNullOrWhiteSpace(tagline))
|
||||
{
|
||||
voiceBits.Add(voice["verbosity"].ToString());
|
||||
}
|
||||
if (voice["tone"] is JArray tones)
|
||||
{
|
||||
voiceBits.AddRange(tones.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
}
|
||||
if (voice["humor"] != null && voice["humor"].ToString() != "none")
|
||||
{
|
||||
voiceBits.Add($"humor:{voice["humor"]}");
|
||||
}
|
||||
if (voice["nsfw"] != null)
|
||||
{
|
||||
voiceBits.Add($"NSFW {voice["nsfw"]}");
|
||||
}
|
||||
if (voice["language"] != null)
|
||||
{
|
||||
voiceBits.Add(voice["language"].ToString());
|
||||
}
|
||||
if (voiceBits.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Voice: " + string.Join(", ", voiceBits));
|
||||
sb.AppendLine($"*{tagline}*");
|
||||
}
|
||||
|
||||
string prefers = FormatCategoryMap(likes);
|
||||
if (!string.IsNullOrWhiteSpace(prefers))
|
||||
foreach (JProperty prop in parts.Properties())
|
||||
{
|
||||
sb.AppendLine("Prefers: " + prefers);
|
||||
}
|
||||
string avoids = FormatCategoryMap(dislikes);
|
||||
if (!string.IsNullOrWhiteSpace(avoids))
|
||||
{
|
||||
sb.AppendLine("Avoids: " + avoids);
|
||||
}
|
||||
|
||||
List<string> ruleBits = [];
|
||||
if (rules["always"] is JArray always)
|
||||
{
|
||||
foreach (string s in always.Select(t => t?.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
if (prop.Name is "persona" or "extra")
|
||||
{
|
||||
ruleBits.Add("always " + s);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (rules["never"] is JArray never)
|
||||
{
|
||||
foreach (string s in never.Select(t => t?.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
if (prop.Value is not JObject shelf || !shelf.Properties().Any())
|
||||
{
|
||||
ruleBits.Add("never " + s);
|
||||
continue;
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"### {TitleCaseShelf(prop.Name)}");
|
||||
AppendTokenMarkdown(sb, shelf, 0);
|
||||
}
|
||||
if (ruleBits.Count > 0)
|
||||
|
||||
string controlsBlock = RenderControlsBlock(id);
|
||||
if (!string.IsNullOrWhiteSpace(controlsBlock))
|
||||
{
|
||||
sb.AppendLine("Rules: " + string.Join("; ", ruleBits));
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(controlsBlock);
|
||||
}
|
||||
|
||||
string extra = parts["extra"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(extra))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(extra.Trim());
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
@@ -791,6 +1148,8 @@ public sealed class AssistentConfig
|
||||
var skills = ListSkills(id);
|
||||
var personas = ListPersonaCatalog();
|
||||
JObject identity = LoadIdentityParts(id);
|
||||
JObject controlsSchema = LoadControlsSchema(id);
|
||||
JObject controlValues = LoadControlValues(id);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
@@ -800,6 +1159,9 @@ public sealed class AssistentConfig
|
||||
["ui"] = ui,
|
||||
["model"] = model,
|
||||
["exact"] = exact,
|
||||
["controls"] = controlsSchema,
|
||||
["control_values"] = controlValues,
|
||||
["persona_source"] = PersonaSource(id),
|
||||
["packs"] = new JArray(packs.Select(p => new JObject
|
||||
{
|
||||
["id"] = p.id,
|
||||
|
||||
Reference in New Issue
Block a user