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
@@ -123,6 +123,63 @@
|
||||
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 {
|
||||
color: var(--dangerous);
|
||||
border-color: var(--dangerous);
|
||||
|
||||
@@ -144,6 +144,10 @@
|
||||
<div id="agentFilesSection" hidden>
|
||||
<button type="button" id="agentFilesBtn" class="add-cmd-btn"><i class="fa fa-folder"></i> Dateien verwalten</button>
|
||||
</div>
|
||||
<div id="agentSkillsSection" hidden>
|
||||
<label>Skills</label>
|
||||
<div class="skill-list" id="agentSkillList"></div>
|
||||
</div>
|
||||
<label>Benutzerdefinierte Befehle</label>
|
||||
<div class="custom-commands" id="agentCommandsList"></div>
|
||||
<button type="button" id="agentAddCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
||||
|
||||
@@ -144,6 +144,35 @@ async function apiGitStatus(sessionId) {
|
||||
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() {
|
||||
const resp = await fetch('/api/backup', { method: 'POST' });
|
||||
if (!resp.ok) throw new Error('Backup fehlgeschlagen');
|
||||
|
||||
@@ -35,6 +35,8 @@ registerPage('agent', {
|
||||
const addCommandBtn = document.getElementById('agentAddCommandBtn');
|
||||
const filesSection = document.getElementById('agentFilesSection');
|
||||
const filesBtn = document.getElementById('agentFilesBtn');
|
||||
const skillsSection = document.getElementById('agentSkillsSection');
|
||||
const skillList = document.getElementById('agentSkillList');
|
||||
|
||||
_agentAvatarData = null;
|
||||
|
||||
@@ -59,6 +61,22 @@ registerPage('agent', {
|
||||
|
||||
// Custom Commands laden
|
||||
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
|
||||
@@ -125,9 +143,17 @@ registerPage('agent', {
|
||||
};
|
||||
|
||||
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 {
|
||||
await apiUpdateAgent(agentId, data);
|
||||
// Skills aktualisieren
|
||||
const selectedSlugs = getSelectedSkills(skillList);
|
||||
await apiSetAgentSkills(agentId, selectedSlugs);
|
||||
}
|
||||
navigateTo('dashboard');
|
||||
});
|
||||
@@ -150,3 +176,29 @@ registerPage('agent', {
|
||||
_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