Implement Skills feature: filesystem-based skills with agent assignment
- Add SkillService: scans data/skills/ directory, parses _meta.json and SKILL.md frontmatter - Add SkillEndpoint: REST API for listing skills and managing agent-skill assignments - Add agent_skills DB table for agent-to-skill mapping (only relation in DB, skills live on filesystem) - Extend SystemPromptBuilder: inject assigned skills' SKILL.md content into system prompt - Extend ChatEndpoint + SessionEndpoint: load agent skills and pass to prompt builder - Frontend: skill checkboxes in agent form, API functions for skill management - Add form.css styling for skill list items
This commit is contained in:
1 parent
b18dc78651
commit
cf6caea2dc
11 files changed
+454
-6
No files matched your search
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Gott.Services;
|
||||
|
||||
public class SkillService
|
||||
{
|
||||
private readonly string _skillsDir;
|
||||
|
||||
public string SkillsDir => _skillsDir;
|
||||
|
||||
public SkillService(string dataDir)
|
||||
{
|
||||
_skillsDir = Path.Combine(dataDir, "skills");
|
||||
Directory.CreateDirectory(_skillsDir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scannt den Skills-Ordner und gibt alle verfügbaren Skills zurück.
|
||||
/// </summary>
|
||||
public List<SkillInfo> GetAllSkills()
|
||||
{
|
||||
var skills = new List<SkillInfo>();
|
||||
if (!Directory.Exists(_skillsDir)) return skills;
|
||||
|
||||
foreach (var dir in Directory.GetDirectories(_skillsDir))
|
||||
{
|
||||
var skill = LoadSkill(dir);
|
||||
if (skill != null) skills.Add(skill);
|
||||
}
|
||||
|
||||
return skills.OrderBy(s => s.Name).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lädt einen einzelnen Skill anhand seines Slugs.
|
||||
/// </summary>
|
||||
public SkillInfo GetSkill(string slug)
|
||||
{
|
||||
var dir = Path.Combine(_skillsDir, slug);
|
||||
if (!Directory.Exists(dir)) return null;
|
||||
return LoadSkill(dir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liest die SKILL.md eines Skills. Gibt null zurück wenn nicht vorhanden.
|
||||
/// </summary>
|
||||
public string GetSkillPrompt(string slug)
|
||||
{
|
||||
var skillMd = Path.Combine(_skillsDir, slug, "SKILL.md");
|
||||
if (!File.Exists(skillMd)) return null;
|
||||
return File.ReadAllText(skillMd);
|
||||
}
|
||||
|
||||
private SkillInfo LoadSkill(string dir)
|
||||
{
|
||||
var slug = Path.GetFileName(dir);
|
||||
var metaFile = Path.Combine(dir, "_meta.json");
|
||||
var skillMd = Path.Combine(dir, "SKILL.md");
|
||||
|
||||
var info = new SkillInfo
|
||||
{
|
||||
Slug = slug,
|
||||
Path = Path.GetFullPath(dir),
|
||||
HasSkillMd = File.Exists(skillMd)
|
||||
};
|
||||
|
||||
// Name/Description aus SKILL.md Frontmatter lesen
|
||||
if (File.Exists(skillMd))
|
||||
{
|
||||
var content = File.ReadAllText(skillMd);
|
||||
var frontmatter = ParseFrontmatter(content);
|
||||
if (frontmatter.TryGetValue("name", out var name)) info.Name = name;
|
||||
if (frontmatter.TryGetValue("description", out var desc)) info.Description = desc;
|
||||
if (frontmatter.TryGetValue("version", out var ver)) info.Version = ver;
|
||||
}
|
||||
|
||||
// _meta.json als Fallback/Ergänzung
|
||||
if (File.Exists(metaFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(metaFile);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
if (string.IsNullOrEmpty(info.Name) && root.TryGetProperty("name", out var n))
|
||||
info.Name = n.GetString();
|
||||
if (string.IsNullOrEmpty(info.Version) && root.TryGetProperty("version", out var v))
|
||||
info.Version = v.GetString();
|
||||
if (root.TryGetProperty("slug", out var s) && !string.IsNullOrEmpty(s.GetString()))
|
||||
info.Slug = s.GetString();
|
||||
}
|
||||
catch { /* Fehler in _meta.json ignorieren */ }
|
||||
}
|
||||
|
||||
// Fallback: Name = Slug
|
||||
if (string.IsNullOrEmpty(info.Name)) info.Name = slug;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parst einen einfachen YAML-Frontmatter-Block (--- ... ---) aus einer Markdown-Datei.
|
||||
/// </summary>
|
||||
private Dictionary<string, string> ParseFrontmatter(string content)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
if (!content.StartsWith("---")) return result;
|
||||
|
||||
var end = content.IndexOf("---", 3, StringComparison.Ordinal);
|
||||
if (end < 0) return result;
|
||||
|
||||
var frontmatter = content.Substring(3, end - 3).Trim();
|
||||
foreach (var line in frontmatter.Split('\n'))
|
||||
{
|
||||
var colon = line.IndexOf(':');
|
||||
if (colon < 0) continue;
|
||||
var key = line.Substring(0, colon).Trim();
|
||||
var value = line.Substring(colon + 1).Trim();
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class SkillInfo
|
||||
{
|
||||
public string Slug { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Path { get; set; }
|
||||
public bool HasSkillMd { get; set; }
|
||||
}
|
||||
Reference in new issue
Block a user