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
@@ -23,7 +23,7 @@ public static class ChatEndpoint
|
|||||||
app.MapPost("/api/chat", HandleChat);
|
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);
|
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
||||||
if (body == null || body.Messages == null || body.Messages.Count == 0)
|
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 markedMessages = await db.GetMarkedMessages(sessionId);
|
||||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||||
var dataDir = memoryService.DataDir;
|
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");
|
var chainId = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public static class SessionEndpoint
|
|||||||
ctx.Response.StatusCode = 204;
|
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();
|
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||||
if (string.IsNullOrEmpty(sessionId))
|
if (string.IsNullOrEmpty(sessionId))
|
||||||
@@ -125,7 +125,8 @@ public static class SessionEndpoint
|
|||||||
var markedMessages = await db.GetMarkedMessages(sessionId);
|
var markedMessages = await db.GetMarkedMessages(sessionId);
|
||||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||||
var dataDir = memoryService.DataDir;
|
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";
|
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
||||||
await ctx.Response.WriteAsync(systemPrompt);
|
await ctx.Response.WriteAsync(systemPrompt);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ builder.Services.AddSingleton(new ToolService());
|
|||||||
builder.Services.AddSingleton(new ImageService());
|
builder.Services.AddSingleton(new ImageService());
|
||||||
builder.Services.AddSingleton(new StreamingTracker());
|
builder.Services.AddSingleton(new StreamingTracker());
|
||||||
builder.Services.AddSingleton(new MemoryService(dataDir));
|
builder.Services.AddSingleton(new MemoryService(dataDir));
|
||||||
|
builder.Services.AddSingleton(new SkillService(dataDir));
|
||||||
builder.Services.AddSingleton(new DatabaseService(Path.Combine(dataDir, "gott.db")));
|
builder.Services.AddSingleton(new DatabaseService(Path.Combine(dataDir, "gott.db")));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -27,5 +28,6 @@ app.MapAgentEndpoints();
|
|||||||
app.MapWorkspaceEndpoint();
|
app.MapWorkspaceEndpoint();
|
||||||
app.MapInfoEndpoint();
|
app.MapInfoEndpoint();
|
||||||
app.MapBackupEndpoint();
|
app.MapBackupEndpoint();
|
||||||
|
app.MapSkillEndpoints();
|
||||||
|
|
||||||
app.Run("http://0.0.0.0:19000");
|
app.Run("http://0.0.0.0:19000");
|
||||||
@@ -134,6 +134,17 @@ public class DatabaseService
|
|||||||
TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1");
|
TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1");
|
||||||
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
|
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
|
||||||
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
|
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
|
||||||
|
conn.Execute(@"
|
||||||
|
CREATE TABLE IF NOT EXISTS agent_skills (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_id TEXT NOT NULL,
|
||||||
|
skill_slug TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
UNIQUE(agent_id, skill_slug)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_skills_agent ON agent_skills(agent_id);
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
||||||
@@ -570,4 +581,51 @@ public class DatabaseService
|
|||||||
ORDER BY m.id",
|
ORDER BY m.id",
|
||||||
new { sessionId });
|
new { sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Agent Skills ──
|
||||||
|
|
||||||
|
public async Task<IEnumerable<string>> GetAgentSkills(string agentId)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
return await conn.QueryAsync<string>(
|
||||||
|
"SELECT skill_slug FROM agent_skills WHERE agent_id = @agentId ORDER BY skill_slug",
|
||||||
|
new { agentId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAgentSkills(string agentId, IEnumerable<string> skillSlugs)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
using var tx = conn.BeginTransaction();
|
||||||
|
|
||||||
|
await conn.ExecuteAsync("DELETE FROM agent_skills WHERE agent_id = @agentId", new { agentId }, tx);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow.ToString("o");
|
||||||
|
foreach (var slug in skillSlugs)
|
||||||
|
{
|
||||||
|
await conn.ExecuteAsync(@"
|
||||||
|
INSERT INTO agent_skills (agent_id, skill_slug, created_at)
|
||||||
|
VALUES (@agentId, @slug, @now)",
|
||||||
|
new { agentId, slug, now }, tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAgentSkill(string agentId, string skillSlug)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
var now = DateTime.UtcNow.ToString("o");
|
||||||
|
await conn.ExecuteAsync(@"
|
||||||
|
INSERT OR IGNORE INTO agent_skills (agent_id, skill_slug, created_at)
|
||||||
|
VALUES (@agentId, @skillSlug, @now)",
|
||||||
|
new { agentId, skillSlug, now });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAgentSkill(string agentId, string skillSlug)
|
||||||
|
{
|
||||||
|
using var conn = CreateConnection();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
"DELETE FROM agent_skills WHERE agent_id = @agentId AND skill_slug = @skillSlug",
|
||||||
|
new { agentId, skillSlug });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace Gott.Services;
|
|||||||
|
|
||||||
public static class SystemPromptBuilder
|
public static class SystemPromptBuilder
|
||||||
{
|
{
|
||||||
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null)
|
public static string Build(AgentRecord agent, SessionRecord session, IEnumerable<MessageRecord> markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable<string> skillSlugs = null, SkillService skillService = null)
|
||||||
{
|
{
|
||||||
var parts = new List<string>();
|
var parts = new List<string>();
|
||||||
|
|
||||||
@@ -69,6 +69,30 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo
|
|||||||
if (!string.IsNullOrWhiteSpace(session.Context))
|
if (!string.IsNullOrWhiteSpace(session.Context))
|
||||||
parts.Add($"## Kontext\n{session.Context}");
|
parts.Add($"## Kontext\n{session.Context}");
|
||||||
|
|
||||||
|
// Skills
|
||||||
|
if (skillSlugs != null && skillService != null)
|
||||||
|
{
|
||||||
|
var skillParts = new List<string>();
|
||||||
|
foreach (var slug in skillSlugs)
|
||||||
|
{
|
||||||
|
var skillPrompt = skillService.GetSkillPrompt(slug);
|
||||||
|
if (string.IsNullOrEmpty(skillPrompt)) continue;
|
||||||
|
|
||||||
|
var skill = skillService.GetSkill(slug);
|
||||||
|
var header = skill != null ? $"### {skill.Name}" : $"### {slug}";
|
||||||
|
if (skill?.Version != null) header += $" (v{skill.Version})";
|
||||||
|
header += $"\nPfad: `{skill?.Path ?? slug}`";
|
||||||
|
skillParts.Add($"{header}\n\n{skillPrompt}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skillParts.Count > 0)
|
||||||
|
{
|
||||||
|
var skillSection = "## Skills\nDu hast folgende Skills zur Verfügung. Nutze sie über das `execute`-Tool wenn der User etwas anfragt, das ein Skill abdeckt.\n\n"
|
||||||
|
+ string.Join("\n\n---\n\n", skillParts);
|
||||||
|
parts.Add(skillSection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Memory-System
|
// Memory-System
|
||||||
if (agent.UseMemory && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
|
if (agent.UseMemory && memoryService != null && !string.IsNullOrWhiteSpace(agent.AgentId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,6 +123,63 @@
|
|||||||
font-size: var(--font-size);
|
font-size: var(--font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Skills ── */
|
||||||
|
|
||||||
|
.skill-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--badge-spacing);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--badge-spacing) var(--box-spacing);
|
||||||
|
background: var(--f2-bg);
|
||||||
|
border: var(--border-width) solid var(--f2-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-version {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-desc {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-empty {
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: var(--box-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-empty code {
|
||||||
|
background: var(--f2-bg);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
.danger-btn {
|
.danger-btn {
|
||||||
color: var(--dangerous);
|
color: var(--dangerous);
|
||||||
border-color: var(--dangerous);
|
border-color: var(--dangerous);
|
||||||
|
|||||||
@@ -144,6 +144,10 @@
|
|||||||
<div id="agentFilesSection" hidden>
|
<div id="agentFilesSection" hidden>
|
||||||
<button type="button" id="agentFilesBtn" class="add-cmd-btn"><i class="fa fa-folder"></i> Dateien verwalten</button>
|
<button type="button" id="agentFilesBtn" class="add-cmd-btn"><i class="fa fa-folder"></i> Dateien verwalten</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="agentSkillsSection" hidden>
|
||||||
|
<label>Skills</label>
|
||||||
|
<div class="skill-list" id="agentSkillList"></div>
|
||||||
|
</div>
|
||||||
<label>Benutzerdefinierte Befehle</label>
|
<label>Benutzerdefinierte Befehle</label>
|
||||||
<div class="custom-commands" id="agentCommandsList"></div>
|
<div class="custom-commands" id="agentCommandsList"></div>
|
||||||
<button type="button" id="agentAddCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
<button type="button" id="agentAddCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
||||||
|
|||||||
@@ -144,6 +144,35 @@ async function apiGitStatus(sessionId) {
|
|||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Skills API ──
|
||||||
|
|
||||||
|
async function apiGetSkills() {
|
||||||
|
const resp = await fetch('/api/skills');
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiGetAgentSkills(agentId) {
|
||||||
|
const resp = await fetch(`/api/agents/${agentId}/skills`);
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiSetAgentSkills(agentId, skills) {
|
||||||
|
const resp = await fetch(`/api/agents/${agentId}/skills`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ skills })
|
||||||
|
});
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiAddAgentSkill(agentId, slug) {
|
||||||
|
return fetch(`/api/agents/${agentId}/skills/${slug}`, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiRemoveAgentSkill(agentId, slug) {
|
||||||
|
return fetch(`/api/agents/${agentId}/skills/${slug}`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
async function apiBackupDb() {
|
async function apiBackupDb() {
|
||||||
const resp = await fetch('/api/backup', { method: 'POST' });
|
const resp = await fetch('/api/backup', { method: 'POST' });
|
||||||
if (!resp.ok) throw new Error('Backup fehlgeschlagen');
|
if (!resp.ok) throw new Error('Backup fehlgeschlagen');
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ registerPage('agent', {
|
|||||||
const addCommandBtn = document.getElementById('agentAddCommandBtn');
|
const addCommandBtn = document.getElementById('agentAddCommandBtn');
|
||||||
const filesSection = document.getElementById('agentFilesSection');
|
const filesSection = document.getElementById('agentFilesSection');
|
||||||
const filesBtn = document.getElementById('agentFilesBtn');
|
const filesBtn = document.getElementById('agentFilesBtn');
|
||||||
|
const skillsSection = document.getElementById('agentSkillsSection');
|
||||||
|
const skillList = document.getElementById('agentSkillList');
|
||||||
|
|
||||||
_agentAvatarData = null;
|
_agentAvatarData = null;
|
||||||
|
|
||||||
@@ -59,6 +61,22 @@ registerPage('agent', {
|
|||||||
|
|
||||||
// Custom Commands laden
|
// Custom Commands laden
|
||||||
loadCommandsIntoUI(commandsList, agent.customCommands);
|
loadCommandsIntoUI(commandsList, agent.customCommands);
|
||||||
|
|
||||||
|
// Skills laden
|
||||||
|
const [allSkills, agentSkills] = await Promise.all([
|
||||||
|
apiGetSkills(),
|
||||||
|
apiGetAgentSkills(agentId)
|
||||||
|
]);
|
||||||
|
const assignedSlugs = agentSkills.map(s => s.slug);
|
||||||
|
skillsSection.hidden = false;
|
||||||
|
renderSkillCheckboxes(skillList, allSkills, assignedSlugs);
|
||||||
|
} else {
|
||||||
|
// Neue Agents: Skills-Liste auch anzeigen
|
||||||
|
const allSkills = await apiGetSkills();
|
||||||
|
if (allSkills.length > 0) {
|
||||||
|
skillsSection.hidden = false;
|
||||||
|
renderSkillCheckboxes(skillList, allSkills, []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent-Dateien öffnen
|
// Agent-Dateien öffnen
|
||||||
@@ -125,9 +143,17 @@ registerPage('agent', {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
await apiCreateAgent(data);
|
const created = await apiCreateAgent(data);
|
||||||
|
// Skills zuweisen falls welche ausgewählt
|
||||||
|
const selectedSlugs = getSelectedSkills(skillList);
|
||||||
|
if (selectedSlugs.length > 0) {
|
||||||
|
await apiSetAgentSkills(created.agentId, selectedSlugs);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await apiUpdateAgent(agentId, data);
|
await apiUpdateAgent(agentId, data);
|
||||||
|
// Skills aktualisieren
|
||||||
|
const selectedSlugs = getSelectedSkills(skillList);
|
||||||
|
await apiSetAgentSkills(agentId, selectedSlugs);
|
||||||
}
|
}
|
||||||
navigateTo('dashboard');
|
navigateTo('dashboard');
|
||||||
});
|
});
|
||||||
@@ -150,3 +176,29 @@ registerPage('agent', {
|
|||||||
_agentAvatarData = null;
|
_agentAvatarData = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function renderSkillCheckboxes(container, allSkills, assignedSlugs) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (allSkills.length === 0) {
|
||||||
|
container.innerHTML = '<div class="skill-empty">Keine Skills verfügbar. Lege Skills in <code>data/skills/</code> ab.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const skill of allSkills) {
|
||||||
|
const checked = assignedSlugs.includes(skill.slug) ? 'checked' : '';
|
||||||
|
const version = skill.version ? ` <span class="skill-version">v${skill.version}</span>` : '';
|
||||||
|
const desc = skill.description ? `<span class="skill-desc">${skill.description}</span>` : '';
|
||||||
|
const row = document.createElement('label');
|
||||||
|
row.className = 'skill-item';
|
||||||
|
row.innerHTML = `
|
||||||
|
<input type="checkbox" value="${skill.slug}" ${checked}>
|
||||||
|
<span class="skill-name">${skill.name}${version}</span>
|
||||||
|
${desc}
|
||||||
|
`;
|
||||||
|
container.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedSkills(container) {
|
||||||
|
return Array.from(container.querySelectorAll('input[type="checkbox"]:checked'))
|
||||||
|
.map(cb => cb.value);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user