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

+3 -2
View File
@@ -23,7 +23,7 @@ public static class ChatEndpoint
app.MapPost("/api/chat", HandleChat);
}
private static async Task HandleChat(HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config, ToolService toolService, DatabaseService db, StreamingTracker streamingTracker, MemoryService memoryService, CancellationToken cancellationToken)
private static async Task HandleChat(HttpRequest req, HttpResponse res, IHttpClientFactory httpFactory, IConfiguration config, ToolService toolService, DatabaseService db, StreamingTracker streamingTracker, MemoryService memoryService, SkillService skillService, CancellationToken cancellationToken)
{
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
if (body == null || body.Messages == null || body.Messages.Count == 0)
@@ -80,7 +80,8 @@ public static class ChatEndpoint
var markedMessages = await db.GetMarkedMessages(sessionId);
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
var dataDir = memoryService.DataDir;
var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir);
var agentSkills = await db.GetAgentSkills(agent.AgentId);
var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService);
var chainId = Guid.NewGuid().ToString("N");
+3 -2
View File
@@ -91,7 +91,7 @@ public static class SessionEndpoint
ctx.Response.StatusCode = 204;
}
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService)
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
{
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
@@ -125,7 +125,8 @@ public static class SessionEndpoint
var markedMessages = await db.GetMarkedMessages(sessionId);
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
var dataDir = memoryService.DataDir;
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir);
var agentSkills = await db.GetAgentSkills(agent.AgentId);
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService);
ctx.Response.ContentType = "text/plain; charset=utf-8";
await ctx.Response.WriteAsync(systemPrompt);
+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;
}
}