Phase 7: Agent Selector — per-agent soul.md + ChromaDB memory filtering

- Agent dropdown UI (chip button + hidden select) in composer header
- Session.agent field persists agent selection across refresh
- soul.md loaded per-agent via ephemeral_system_prompt injection
- ChromaDB memory filtered by agent topic (lotus/, sunflower/, etc.)
- Fixed streaming.py: agent→_ai_agent variable shadowing (lines 1161, 1163)
- New API endpoints: /api/agents/topology, /api/agents/memory/search
- Agent metadata registry with emoji, name, description per Tier-2 agent
This commit is contained in:
Rose
2026-04-20 17:34:58 +02:00
parent 00045314f8
commit c705fad626
14 changed files with 2578 additions and 320 deletions

View File

@@ -91,6 +91,7 @@ async function populateModelDropdown(){
_applyModelToDropdown(data.default_model, sel);
}
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
// Kick off a background live-model fetch for the active provider.
// This runs after the static list is already shown (no blocking flicker).
if(data.active_provider) _fetchLiveModels(data.active_provider, sel);
@@ -98,6 +99,7 @@ async function populateModelDropdown(){
// API unavailable -- keep the hardcoded HTML options as fallback
console.warn('Failed to load models from server:',e.message);
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
}
}
@@ -148,6 +150,7 @@ async function _fetchLiveModels(provider, sel){
// Restore selection
if(currentVal) _applyModelToDropdown(currentVal, sel);
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
console.log('[hermes] Live models loaded for',provider+':',added,'new models added');
}
}catch(e){
@@ -315,6 +318,108 @@ window.addEventListener('resize',()=>{
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
});
// ── Agent selector dropdown ─────────────────────────────────────────────────
const AGENT_META = {
rose: {emoji:'🌹', name:'Rose', domain:'Orchestrator & Main Interface'},
lotus: {emoji:'🪷', name:'Lotus', domain:'Health, Fitness & Recovery'},
'forget-me-not':{emoji:'🌼', name:'Forget-me-not', domain:'Calendar, Time & Social'},
sunflower: {emoji:'🌻', name:'Sunflower', domain:'Finance, Wealth & Subscriptions'},
iris: {emoji:'⚜️', name:'Iris', domain:'Career, Learning & Focus'},
ivy: {emoji:'🌿', name:'Ivy', domain:'Smart Home & Environment'},
dandelion: {emoji:'🛡️', name:'Dandelion', domain:'Communication Triage'},
root: {emoji:'🌳', name:'Root', domain:'DevOps, Logs & System Health'},
};
function renderAgentDropdown(){
const dd=$('composerAgentDropdown');
const sel=$('agentSelect');
if(!dd||!sel) return;
const current=sel.value;
const groups={'Tier-1':['rose'],'Tier-2':['lotus','forget-me-not','sunflower','iris','ivy','dandelion','root']};
let html='';
for(const [grp,ids] of Object.entries(groups)){
html+=`<div class="model-group">${grp}</div>`;
for(const id of ids){
const m=AGENT_META[id];
const active=id===current?' active':'';
html+=`<div class="agent-opt${active}" onclick="selectAgentFromDropdown('${id}')">
<span class="agent-opt-name">${m.emoji} ${m.name}</span>
<span class="agent-opt-domain">${m.domain}</span>
</div>`;
}
}
dd.innerHTML=html;
}
function toggleAgentDropdown(){
const dd=$('composerAgentDropdown');
const chip=$('composerAgentChip');
if(!dd||!chip) return;
if(dd.classList.contains('open')){
dd.classList.remove('open');
chip.classList.remove('active');
return;
}
closeModelDropdown();
if(typeof closeProfileDropdown==='function') closeProfileDropdown();
if(typeof closeWsDropdown==='function') closeWsDropdown();
renderAgentDropdown();
dd.classList.add('open');
chip.classList.add('active');
// position below chip
const chipRect=chip.getBoundingClientRect();
const wrap=chip.closest('.composer-agent-wrap');
const wrapRect=wrap.getBoundingClientRect();
dd.style.left=(chipRect.left-wrapRect.left)+'px';
}
function closeAgentDropdown(){
const dd=$('composerAgentDropdown');
const chip=$('composerAgentChip');
if(dd) dd.classList.remove('open');
if(chip) chip.classList.remove('active');
}
function selectAgentFromDropdown(value){
const sel=$('agentSelect');
if(!sel) return;
sel.value=value;
syncAgentChip();
closeAgentDropdown();
// Save to session / localStorage
if(typeof S!=='undefined'&&S.session) S.session.agent=value;
try{localStorage.setItem('hermes-webui-agent',value);}catch(e){}
}
function syncAgentChip(){
const sel=$('agentSelect');
const icon=$('composerAgentIcon');
const label=$('composerAgentLabel');
if(!sel||!icon||!label) return;
const m=AGENT_META[sel.value]||AGENT_META.rose;
icon.textContent=m.emoji;
label.textContent=m.name;
}
// Init agent chip from localStorage on load
window.addEventListener('DOMContentLoaded',()=>{
try{
const saved=localStorage.getItem('hermes-webui-agent');
if(saved){
const sel=$('agentSelect');
if(sel&&Array.from(sel.options).some(o=>o.value===saved)){
sel.value=saved;
}
}
}catch(e){}
syncAgentChip();
});
document.addEventListener('click',e=>{
if(!e.target.closest('#composerAgentChip') && !e.target.closest('#composerAgentDropdown')) closeAgentDropdown();
});
// ── Scroll pinning ──────────────────────────────────────────────────────────
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
// Once the user scrolls back to within 80px of the bottom, re-pin.
@@ -1033,6 +1138,7 @@ function syncTopbar(){
document.title=window._botName||'Hermes';
if(typeof syncWorkspaceDisplays==='function') syncWorkspaceDisplays();
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
if(typeof _syncHermesPanelSessionActions==='function') _syncHermesPanelSessionActions();
else {
const sidebarName=$('sidebarWsName');
@@ -1072,6 +1178,7 @@ function syncTopbar(){
}
}
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncAgentChip==='function') syncAgentChip();
// Show Clear button only when session has messages
const clearBtn=$('btnClearConv');
if(clearBtn) clearBtn.style.display=(S.messages&&S.messages.filter(msg=>msg.role!=='tool').length>0)?'':'none';
@@ -1283,10 +1390,6 @@ function renderMessages(){
scrollToBottom();
// Apply syntax highlighting after DOM is built
requestAnimationFrame(()=>{highlightCode();addCopyButtons();renderMermaidBlocks();renderKatexBlocks();});
// Refresh todo panel if it's currently open
if(typeof loadTodos==='function' && document.getElementById('panelTodos') && document.getElementById('panelTodos').classList.contains('active')){
loadTodos();
}
}
function toolIcon(name){