refactor: Implement dynamic skill loading from definitions, replacing hardcoded skills, and add a new skill management command.
This commit is contained in:
124
Commands/SkillCommand.cs
Normal file
124
Commands/SkillCommand.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Toak.Core.Skills;
|
||||
using Toak.Serialization;
|
||||
|
||||
namespace Toak.Commands;
|
||||
|
||||
public static class SkillCommand
|
||||
{
|
||||
public static Command CreateCommand(Option<bool> verboseOption)
|
||||
{
|
||||
var skillCmd = new Command("skill", "Manage dynamic skills (list, add, remove)");
|
||||
|
||||
var listCmd = new Command("list", "List all available skills");
|
||||
listCmd.SetHandler(ExecuteListAsync);
|
||||
skillCmd.AddCommand(listCmd);
|
||||
|
||||
// Add
|
||||
var addCmd = new Command("add", "Add a new skill interactively");
|
||||
addCmd.SetHandler(ExecuteAddAsync);
|
||||
skillCmd.AddCommand(addCmd);
|
||||
|
||||
// Remove
|
||||
var removeCmd = new Command("remove", "Remove a skill");
|
||||
var nameArg = new Argument<string>("name", "The name of the skill to remove");
|
||||
removeCmd.AddArgument(nameArg);
|
||||
removeCmd.SetHandler((name) => ExecuteRemoveAsync(name), nameArg);
|
||||
skillCmd.AddCommand(removeCmd);
|
||||
|
||||
return skillCmd;
|
||||
}
|
||||
|
||||
private static async Task ExecuteListAsync()
|
||||
{
|
||||
SkillRegistry.Initialize();
|
||||
|
||||
var table = new Table().Border(TableBorder.Rounded);
|
||||
table.AddColumn("Name");
|
||||
table.AddColumn("Action");
|
||||
table.AddColumn("Hotwords");
|
||||
table.AddColumn("Description");
|
||||
table.AddColumn("ScriptPath");
|
||||
|
||||
foreach (var skill in SkillRegistry.AllSkills)
|
||||
{
|
||||
var def = JsonSerializer.Deserialize<SkillDefinition>(
|
||||
File.ReadAllText(Path.Combine(SkillRegistry.SkillsDirectory, $"{skill.Name.ToLowerInvariant()}.json")),
|
||||
AppJsonSerializerContext.Default.SkillDefinition);
|
||||
|
||||
if (def == null) continue;
|
||||
|
||||
table.AddRow(
|
||||
$"[green]{def.Name}[/]",
|
||||
!string.IsNullOrEmpty(def.Action) ? $"[yellow]{def.Action}[/]" : "type",
|
||||
$"[blue]{string.Join(", ", def.Hotwords)}[/]",
|
||||
def.Description,
|
||||
def.Action == "script" ? $"[dim]{def.ScriptPath ?? "None"}[/]" : "-"
|
||||
);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
}
|
||||
|
||||
private static async Task ExecuteAddAsync()
|
||||
{
|
||||
AnsiConsole.MarkupLine("[bold blue]Add a new Dynamic Skill[/]");
|
||||
|
||||
var name = AnsiConsole.Ask<string>("Skill [green]Name[/]:");
|
||||
var description = AnsiConsole.Ask<string>("Skill [green]Description[/]:");
|
||||
|
||||
var hotwordsStr = AnsiConsole.Ask<string>("Comma-separated [green]Hotwords[/] (e.g. 'System my skill, System do skill'):");
|
||||
var hotwords = hotwordsStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var action = AnsiConsole.Prompt(
|
||||
new SelectionPrompt<string>()
|
||||
.Title("What is the [green]Action[/] type?")
|
||||
.AddChoices(new[] { "type", "script" })
|
||||
);
|
||||
|
||||
string? scriptPath = null;
|
||||
if (action == "script")
|
||||
{
|
||||
scriptPath = AnsiConsole.Ask<string>("Enter the absolute [green]Script Path[/] (~/ is allowed):");
|
||||
}
|
||||
|
||||
var systemPrompt = AnsiConsole.Ask<string>("Enter the [green]System Prompt[/] (use {transcript} to inject the user's speech):");
|
||||
|
||||
var def = new SkillDefinition
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Hotwords = hotwords,
|
||||
Action = action,
|
||||
ScriptPath = scriptPath,
|
||||
SystemPrompt = systemPrompt
|
||||
};
|
||||
|
||||
SkillRegistry.Initialize(); // ensure dir exists
|
||||
string filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json");
|
||||
string json = JsonSerializer.Serialize(def, AppJsonSerializerContext.Default.SkillDefinition);
|
||||
File.WriteAllText(filename, json);
|
||||
|
||||
AnsiConsole.MarkupLine($"[bold green]Success![/] Skill '{name}' saved to {filename}");
|
||||
}
|
||||
|
||||
private static async Task ExecuteRemoveAsync(string name)
|
||||
{
|
||||
SkillRegistry.Initialize();
|
||||
var filename = Path.Combine(SkillRegistry.SkillsDirectory, $"{name.ToLowerInvariant()}.json");
|
||||
if (File.Exists(filename))
|
||||
{
|
||||
File.Delete(filename);
|
||||
AnsiConsole.MarkupLine($"[bold green]Success![/] Deleted skill '{name}' ({filename})");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[bold red]Error:[/] Skill file not found: {filename}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user