1
0

refactor: Implement dynamic skill loading from definitions, replacing hardcoded skills, and add a new skill management command.

This commit is contained in:
2026-02-28 13:43:23 +01:00
parent a1037edb29
commit 7b144aedd7
11 changed files with 310 additions and 125 deletions

View File

@@ -0,0 +1,53 @@
using System;
using System.Diagnostics;
namespace Toak.Core.Skills;
public class DynamicSkill : ISkill
{
private readonly SkillDefinition _def;
public string Name => _def.Name;
public string Description => _def.Description;
public string[] Hotwords => _def.Hotwords;
public bool HandlesExecution => _def.Action.ToLowerInvariant() == "script";
public DynamicSkill(SkillDefinition def)
{
_def = def;
}
public string GetSystemPrompt(string rawTranscript)
{
return _def.SystemPrompt.Replace("{transcript}", rawTranscript);
}
public void Execute(string llmResult)
{
if (HandlesExecution && !string.IsNullOrWhiteSpace(_def.ScriptPath))
{
var expandedPath = Environment.ExpandEnvironmentVariables(_def.ScriptPath);
if (expandedPath.StartsWith("~"))
{
expandedPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + expandedPath.Substring(1);
}
try
{
var process = Process.Start(new ProcessStartInfo
{
FileName = expandedPath,
Arguments = $"\"{llmResult.Replace("\"", "\\\"")}\"",
UseShellExecute = false,
CreateNoWindow = true
});
process?.WaitForExit();
}
catch (Exception ex)
{
Console.WriteLine($"[DynamicSkill] Error executing script '{expandedPath}': {ex.Message}");
}
}
}
}

View File

@@ -1,26 +0,0 @@
namespace Toak.Core.Skills;
public class ProfessionalSkill : ISkill
{
public string Name => "Professional";
public string Description => "Rewrites the spoken text to sound highly professional and articulate.";
public string[] Hotwords => new[] { "System professional", "System rewrite professionally", "System formalize" };
public bool HandlesExecution => false;
public string GetSystemPrompt(string rawTranscript)
{
return @"You are an expert formal editor and corporate communicator.
The user wants to rewrite the following text professionally. The transcript might start with a hotword like 'System professional'.
- Enhance the text from the speaker to sound highly professional and articulate.
- Maintain the exact meaning and key information of the original transcription.
- Ensure paragraph breaks are added logically to prevent walls of text, improving readability.
- Avoid filler words, hesitations (umm, uh), or conversational redundancies.
- Output ONLY the final polished text. Do not include markdown, explanations, or quotes.";
}
public void Execute(string llmResult)
{
// Not used since HandlesExecution is false
}
}

View File

@@ -0,0 +1,11 @@
namespace Toak.Core.Skills;
public class SkillDefinition
{
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public string[] Hotwords { get; set; } = System.Array.Empty<string>();
public string Action { get; set; } = "type"; // "type" or "script"
public string SystemPrompt { get; set; } = "";
public string? ScriptPath { get; set; }
}

View File

@@ -1,19 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Toak.Serialization;
namespace Toak.Core.Skills;
public static class SkillRegistry
{
public static readonly ISkill[] AllSkills = new ISkill[]
public static List<ISkill> AllSkills = new List<ISkill>();
public static string SkillsDirectory => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "toak", "skills");
public static void Initialize()
{
new TerminalSkill(),
new TranslateSkill(),
new ProfessionalSkill(),
new SummarySkill()
};
if (!Directory.Exists(SkillsDirectory))
{
Directory.CreateDirectory(SkillsDirectory);
CreateDefaultSkills();
}
AllSkills.Clear();
foreach (var file in Directory.GetFiles(SkillsDirectory, "*.json"))
{
try
{
string json = File.ReadAllText(file);
var def = JsonSerializer.Deserialize(json, AppJsonSerializerContext.Default.SkillDefinition);
if (def != null)
{
AllSkills.Add(new DynamicSkill(def));
}
}
catch (Exception ex)
{
Logger.LogDebug($"Failed to load skill from {file}: {ex.Message}");
}
}
}
public static ISkill? DetectSkill(string transcript, IEnumerable<string> activeSkillNames)
{
var activeSkills = AllSkills.Where(s => activeSkillNames.Contains(s.Name, StringComparer.OrdinalIgnoreCase)).ToList();
if (AllSkills.Count == 0) Initialize();
var activeSkills = AllSkills.Where(s => activeSkillNames.Contains(s.Name, StringComparer.OrdinalIgnoreCase)).ToList();
string normalizedTranscript = transcript.Trim();
foreach (var skill in activeSkills)
@@ -28,4 +61,71 @@ public static class SkillRegistry
}
return null;
}
private static void CreateDefaultSkills()
{
var defaults = new List<SkillDefinition>
{
new SkillDefinition
{
Name = "Terminal",
Description = "Executes the spoken command in your shell.",
Hotwords = new[] { "System terminal", "System run", "System execute" },
Action = "script",
ScriptPath = "~/.config/toak/skills/terminal_action.sh",
SystemPrompt = "You are a Linux terminal expert. Translate the user's request into a single, valid bash command. Output ONLY the raw command, no formatting, no markdown."
},
new SkillDefinition
{
Name = "Translate",
Description = "Translates the spoken text into another language on the fly.",
Hotwords = new[] { "System translate to", "System translate into" },
Action = "type",
SystemPrompt = @"You are an expert translator. The user wants to translate the following text.
The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:').
Translate the REST of the transcript into that target language.
Output ONLY the final translated text. Do not include markdown, explanations, or quotes."
},
new SkillDefinition
{
Name = "Professional",
Description = "Rewrites text into a formal, articulate tone.",
Hotwords = new[] { "System professional", "System formalize", "System formal" },
Action = "type",
SystemPrompt = "Rewrite the following text to be articulate and formal. Do not add any conversational filler. Text: {transcript}"
},
new SkillDefinition
{
Name = "Summary",
Description = "Provides a direct, crisp summary of the dictation.",
Hotwords = new[] { "System summary", "System concise", "System summarize" },
Action = "type",
SystemPrompt = "Summarize the following text to be as concise and direct as possible. Remove all fluff. Text: {transcript}"
}
};
foreach (var def in defaults)
{
string filename = Path.Combine(SkillsDirectory, $"{def.Name.ToLowerInvariant()}.json");
string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
File.WriteAllText(filename, json);
}
// Create the default terminal wrapper script if it doesn't exist
string scriptPath = Path.Combine(SkillsDirectory, "terminal_action.sh");
if (!File.Exists(scriptPath))
{
File.WriteAllText(scriptPath, "#!/bin/bash\n\n# Terminal skill wrapper script.\n# The LLM output is passed as the first argument ($1).\nx-terminal-emulator -e bash -c \"$1; exec bash\"\n");
// Try to make it executable
try {
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo {
FileName = "chmod",
Arguments = $"+x \"{scriptPath}\"",
CreateNoWindow = true,
UseShellExecute = false
})?.WaitForExit();
} catch { }
}
}
}

View File

@@ -1,25 +0,0 @@
namespace Toak.Core.Skills;
public class SummarySkill : ISkill
{
public string Name => "Summary";
public string Description => "Summarizes the spoken text securely and concisely, removing fluff.";
public string[] Hotwords => new[] { "System summary", "System summarize", "System concise" };
public bool HandlesExecution => false;
public string GetSystemPrompt(string rawTranscript)
{
return @"You are an expert editor who strips all fluff and makes text as concise as possible.
The user wants to summarize the following text. The transcript might start with a hotword like 'System summary'.
- Strip all fluff, filler, and unnecessary conversational words.
- Make the output as direct and brief as possible without losing the core information.
- Use clear, crisp phrasing. If the text lists items or instructions, format them logically.
- Output ONLY the final summarized text. Do not include markdown, explanations, or quotes.";
}
public void Execute(string llmResult)
{
// Not used since HandlesExecution is false
}
}

View File

@@ -1,43 +0,0 @@
using System.Diagnostics;
namespace Toak.Core.Skills;
public class TerminalSkill : ISkill
{
public string Name => "Terminal";
public string Description => "Translates an intent into a bash command and runs it in the background.";
public string[] Hotwords => new[] { "System terminal", "System command" };
public bool HandlesExecution => false;
public string GetSystemPrompt(string rawTranscript)
{
return @"You are a command-line assistant. The user will ask you to perform a task.
Translate the request into a single bash command.
Output ONLY the raw bash command to achieve this task. Do not include markdown formatting, backticks, or explanations.";
}
public void Execute(string llmResult)
{
// HandlesExecution is false because we are not retarded enough
// to let the LLM execute commands directly
try
{
Console.WriteLine($"[TerminalSkill] Executing: {llmResult}");
var escapedCmd = llmResult.Replace("\"", "\\\"");
var pInfo = new ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"{escapedCmd}\"",
UseShellExecute = false,
CreateNoWindow = true
};
Process.Start(pInfo);
IO.Notifications.Notify("Toak Terminal Executed", llmResult);
}
catch (Exception ex)
{
Console.WriteLine($"[TerminalSkill Error] {ex.Message}");
}
}
}

View File

@@ -1,23 +0,0 @@
namespace Toak.Core.Skills;
public class TranslateSkill : ISkill
{
public string Name => "Translate";
public string Description => "Translates the spoken text into another language on the fly.";
public string[] Hotwords => new[] { "System translate to", "System translate into" };
public bool HandlesExecution => false;
public string GetSystemPrompt(string rawTranscript)
{
return @"You are an expert translator. The user wants to translate the following text.
The first few words identify the target language (e.g. 'Translate to Spanish:', 'Translate into Hungarian:').
Translate the REST of the transcript into that target language.
Output ONLY the final translated text. Do not include markdown, explanations, or quotes.";
}
public void Execute(string llmResult)
{
// Not used since HandlesExecution is false
}
}