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:
Meleki committed 2026-03-01 17:22:14 +01:00
1 parent b18dc78651
commit cf6caea2dc
11 files changed
+454 -6

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Gott.Endpoints;
public static class SkillEndpoint
{
public static void MapSkillEndpoints(this IEndpointRouteBuilder app)
{
// Skills (Dateisystem-basiert)
app.MapGet("/api/skills", HandleListSkills);
// Agent ↔ Skill Zuordnung
app.MapGet("/api/agents/{agentId}/skills", HandleGetAgentSkills);
app.MapPut("/api/agents/{agentId}/skills", HandleSetAgentSkills);
app.MapPost("/api/agents/{agentId}/skills/{slug}", HandleAddAgentSkill);
app.MapDelete("/api/agents/{agentId}/skills/{slug}", HandleRemoveAgentSkill);
}
private static async Task HandleListSkills(HttpContext ctx, SkillService skillService)
{
var skills = skillService.GetAllSkills();
await ctx.Response.WriteAsJsonAsync(skills);
}
private static async Task HandleGetAgentSkills(HttpContext ctx, DatabaseService db, SkillService skillService)
{
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
var slugs = await db.GetAgentSkills(agentId);
var allSkills = skillService.GetAllSkills();
// Nur Skills zurückgeben die auch wirklich existieren
var result = allSkills
.Where(s => slugs.Contains(s.Slug))
.ToList();
await ctx.Response.WriteAsJsonAsync(result);
}
private static async Task HandleSetAgentSkills(HttpContext ctx, DatabaseService db)
{
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
if (string.IsNullOrEmpty(agentId)) { ctx.Response.StatusCode = 400; return; }
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
var slugs = body.TryGetProperty("skills", out var arr)
? arr.EnumerateArray().Select(e => e.GetString()).Where(s => !string.IsNullOrEmpty(s)).ToList()
: new System.Collections.Generic.List<string>();
await db.SetAgentSkills(agentId, slugs);
var updated = await db.GetAgentSkills(agentId);
await ctx.Response.WriteAsJsonAsync(updated);
}
private static async Task HandleAddAgentSkill(HttpContext ctx, DatabaseService db)
{
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
var slug = ctx.Request.RouteValues["slug"]?.ToString();
if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug)) { ctx.Response.StatusCode = 400; return; }
await db.AddAgentSkill(agentId, slug);
ctx.Response.StatusCode = 204;
}
private static async Task HandleRemoveAgentSkill(HttpContext ctx, DatabaseService db)
{
var agentId = ctx.Request.RouteValues["agentId"]?.ToString();
var slug = ctx.Request.RouteValues["slug"]?.ToString();
if (string.IsNullOrEmpty(agentId) || string.IsNullOrEmpty(slug)) { ctx.Response.StatusCode = 400; return; }
await db.RemoveAgentSkill(agentId, slug);
ctx.Response.StatusCode = 204;
}
}