156 lines
6.3 KiB
C#
156 lines
6.3 KiB
C#
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 List<ISkill> AllSkills = new List<ISkill>();
|
|
|
|
public static string SkillsDirectory => Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
".config", "toak", "skills");
|
|
|
|
public static void Initialize()
|
|
{
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
foreach (var hotword in skill.Hotwords)
|
|
{
|
|
if (normalizedTranscript.StartsWith(hotword, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return skill;
|
|
}
|
|
}
|
|
}
|
|
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.
|
|
The text will start with 'System professional', 'System formalize', or 'System formal',
|
|
or something along the lines of that. You can ignore those words.
|
|
Do not add any conversational filler.
|
|
Make sure to preserve the meaning of the original text.
|
|
Output ONLY the final professional text.
|
|
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.
|
|
The text will start with 'System summary', 'System concise', or 'System summarize',
|
|
and you shoul ignore that part of the text.
|
|
Output ONLY the final summary text.
|
|
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))
|
|
{
|
|
string scriptContent = "#!/bin/bash\n" +
|
|
"export TOAK_PROPOSED_CMD=\"$1\"\n" +
|
|
"x-terminal-emulator -e bash -c 'echo -e \"\\033[1;32mToak Terminal Skill\\033[0m\"; " +
|
|
"echo \"Proposed command:\"; echo; " +
|
|
"echo -e \" \\033[33m$TOAK_PROPOSED_CMD\\033[0m\"; echo; " +
|
|
"read -p \"Execute command? [Y/n] \" resp; " +
|
|
"if [[ $resp =~ ^[Yy]$ ]] || [[ -z $resp ]]; then " +
|
|
"echo; echo -e \"\\033[1;36m>> Executing...\\033[0m\"; eval \"$TOAK_PROPOSED_CMD\"; " +
|
|
"else echo; echo \"Aborted.\"; fi; " +
|
|
"echo; echo \"Process finished. Press Enter to exit.\"; read;'\n";
|
|
|
|
File.WriteAllText(scriptPath, scriptContent);
|
|
|
|
// 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 { }
|
|
}
|
|
}
|
|
}
|