feat(councils-ui): add on-demand council conversations panel and model config controls
This commit is contained in:
@@ -14,6 +14,9 @@ let _assistantSaveState = null;
|
||||
let _lastAssistantConfig = null;
|
||||
let _assistantManualOverrides = new Set();
|
||||
let _assistantModelDefaultsDraft = null;
|
||||
let _lastCouncilTask = '';
|
||||
let _lastCouncilResult = null;
|
||||
let _lastCouncilError = null;
|
||||
let _lastServices = [];
|
||||
let _serviceConfigState = {
|
||||
open: false,
|
||||
@@ -216,6 +219,22 @@ function renderAssistantSaveState() {
|
||||
return `<div id="ops-assistant-status" class="text-sm ${toneClass} mt-4">${escapeHtml(_assistantSaveState.message)} <span class="text-zinc-500">(at ${escapeHtml(at)})</span></div>`;
|
||||
}
|
||||
|
||||
function extractCouncilResultFromOutput(output) {
|
||||
if (typeof output !== 'string' || output.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
const marker = '{"pipeline_version"';
|
||||
const idx = output.lastIndexOf(marker);
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(output.slice(idx));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Initial full render ─────────────────────────────────────────
|
||||
|
||||
function renderSkeleton(el) {
|
||||
@@ -690,6 +709,64 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerCouncilRun(task, statusEl) {
|
||||
if (!_dashboardClient) {return false;}
|
||||
if (!task) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'Task is required.';
|
||||
statusEl.className = 'text-sm text-red-500';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'Running council...';
|
||||
statusEl.className = 'text-sm text-zinc-500';
|
||||
}
|
||||
try {
|
||||
const result = await _dashboardClient.call('tools.invoke', {
|
||||
tool: 'council.run',
|
||||
args: { task },
|
||||
});
|
||||
if (!result?.success) {
|
||||
_lastCouncilError = result?.error ?? 'Council run failed.';
|
||||
_lastCouncilResult = null;
|
||||
if (statusEl) {
|
||||
statusEl.textContent = _lastCouncilError;
|
||||
statusEl.className = 'text-sm text-red-500';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = extractCouncilResultFromOutput(result.output);
|
||||
if (!parsed) {
|
||||
_lastCouncilError = 'Council run succeeded but output could not be parsed.';
|
||||
_lastCouncilResult = null;
|
||||
if (statusEl) {
|
||||
statusEl.textContent = _lastCouncilError;
|
||||
statusEl.className = 'text-sm text-amber-500';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastCouncilTask = task;
|
||||
_lastCouncilResult = parsed;
|
||||
_lastCouncilError = null;
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Council run complete: ${parsed.stop_snapshot?.stop_reason ?? 'ok'}`;
|
||||
statusEl.className = 'text-sm text-green-500';
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
_lastCouncilError = `Council run error: ${error instanceof Error ? error.message : String(error)}`;
|
||||
_lastCouncilResult = null;
|
||||
if (statusEl) {
|
||||
statusEl.textContent = _lastCouncilError;
|
||||
statusEl.className = 'text-sm text-red-500';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAssistantHealth(configData) {
|
||||
const el = document.getElementById('ops-assistant-health');
|
||||
if (!el) {return;}
|
||||
@@ -723,6 +800,11 @@ function updateAssistantHealth(configData) {
|
||||
const modelTier = _assistantModelDefaultsDraft?.primaryTier ?? configData?.agents?.primary_tier ?? 'default';
|
||||
const delegation = configData?.agents?.delegation ?? {};
|
||||
const backgroundModels = configData?.agents?.background_models ?? {};
|
||||
const councils = configData?.councils ?? {};
|
||||
const councilsDefaults = councils.defaults ?? {};
|
||||
const councilsGroups = councils.groups ?? {};
|
||||
const councilsD = councilsGroups.D ?? {};
|
||||
const councilsP = councilsGroups.P ?? {};
|
||||
const tiers = configData?.models ?? {};
|
||||
const modelCatalog = configData?.__modelCatalog ?? [];
|
||||
const providerList = modelCatalog.length > 0
|
||||
@@ -763,6 +845,10 @@ function updateAssistantHealth(configData) {
|
||||
{ key: 'tool_summarisation', label: 'Tool summarisation' },
|
||||
{ key: 'complex_reasoning', label: 'Complex reasoning' },
|
||||
];
|
||||
const councilConversations = Array.isArray(_lastCouncilResult?.conversations) ? _lastCouncilResult.conversations : [];
|
||||
const councilSummary = _lastCouncilResult?.stop_snapshot
|
||||
? `Last run: ${_lastCouncilResult.stop_snapshot.stop_reason} (round ${_lastCouncilResult.stop_snapshot.round_reached})`
|
||||
: (_lastCouncilError ? `Last run failed: ${_lastCouncilError}` : 'No council run yet in this dashboard session.');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2 mb-4">
|
||||
@@ -898,6 +984,97 @@ function updateAssistantHealth(configData) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
|
||||
<div class="text-sm font-semibold text-zinc-50 mb-3">Councils</div>
|
||||
<div class="text-sm text-zinc-500 mb-3">On-demand council orchestration settings and council role model tiers.</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
<label class="flex items-center gap-2 mt-5 md:mt-0">
|
||||
<input id="assist-councils-enabled" type="checkbox" ${councils.enabled ? 'checked' : ''} />
|
||||
<span class="text-xs text-zinc-400">Enable councils</span>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">D model tier</span>
|
||||
<select id="assist-council-d-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none">
|
||||
${tierOption(councilsD.model_tier ?? 'complex')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">P model tier</span>
|
||||
<select id="assist-council-p-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none">
|
||||
${tierOption(councilsP.model_tier ?? 'complex')}
|
||||
</select>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">Meta model tier</span>
|
||||
<select id="assist-council-meta-tier" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none">
|
||||
${tierOption(councils.meta_model_tier ?? 'complex')}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">D arbiter agent</span>
|
||||
<input id="assist-council-d-arbiter" type="text" value="${escapeHtml(councilsD.arbiter_agent ?? 'council_d_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">D freethinker agent</span>
|
||||
<input id="assist-council-d-freethinker" type="text" value="${escapeHtml(councilsD.freethinker_agent ?? 'council_d_freethinker')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">P arbiter agent</span>
|
||||
<input id="assist-council-p-arbiter" type="text" value="${escapeHtml(councilsP.arbiter_agent ?? 'council_p_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">P freethinker agent</span>
|
||||
<input id="assist-council-p-freethinker" type="text" value="${escapeHtml(councilsP.freethinker_agent ?? 'council_p_freethinker')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">Meta arbiter agent</span>
|
||||
<input id="assist-council-meta-arbiter" type="text" value="${escapeHtml(councils.meta_arbiter_agent ?? 'council_meta_arbiter')}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">Scaffold path (optional)</span>
|
||||
<input id="assist-council-scaffold" type="text" value="${escapeHtml(councils.scaffold_path ?? '')}" placeholder="docs/councils/ai-council-production-scaffold.json" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-sm text-zinc-400">Max rounds</span>
|
||||
<input id="assist-council-max-rounds" type="number" min="1" max="6" value="${escapeHtml(String(councilsDefaults.max_rounds ?? 2))}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-councils">
|
||||
Save Councils
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 p-3 border border-zinc-800 rounded-md bg-zinc-950/60">
|
||||
<div class="text-sm font-semibold text-zinc-50 mb-2">Council Conversations</div>
|
||||
<div class="text-xs text-zinc-500 mb-3">${escapeHtml(councilSummary)}</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-2 mb-3">
|
||||
<input id="assist-council-task" type="text" value="${escapeHtml(_lastCouncilTask)}" placeholder="Run councils on demand: e.g. design a 2-week experiment plan..." class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="run-council">
|
||||
Run Council
|
||||
</button>
|
||||
</div>
|
||||
<div id="assist-council-status" class="text-sm text-zinc-500 mb-3"></div>
|
||||
<div class="max-h-72 overflow-y-auto space-y-2">
|
||||
${councilConversations.length === 0
|
||||
? '<div class="text-sm text-zinc-500">No conversation log yet.</div>'
|
||||
: councilConversations.map((turn, idx) => `
|
||||
<details class="border border-zinc-800 rounded-md bg-zinc-900/70 p-2">
|
||||
<summary class="cursor-pointer text-sm text-zinc-100">
|
||||
#${idx + 1} ${escapeHtml(turn.call_id)} · ${escapeHtml(turn.agent)} @ ${escapeHtml(turn.tier)}
|
||||
</summary>
|
||||
<div class="mt-2 text-xs text-zinc-400">Prompt payload</div>
|
||||
<pre class="text-xs text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded p-2 mt-1">${escapeHtml(JSON.stringify(turn.prompt_payload, null, 2))}</pre>
|
||||
<div class="mt-2 text-xs text-zinc-400">Response</div>
|
||||
<pre class="text-xs text-zinc-300 whitespace-pre-wrap bg-zinc-950 border border-zinc-800 rounded p-2 mt-1">${escapeHtml(String(turn.response ?? ''))}</pre>
|
||||
</details>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
|
||||
<div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Activation Checklist</div>
|
||||
<div class="space-y-1 mb-4">
|
||||
@@ -1064,6 +1241,49 @@ function updateAssistantHealth(configData) {
|
||||
}
|
||||
patches[`agents.background_models.${task}.fallback_tier`] = fallbackTier;
|
||||
}
|
||||
} else if (action === 'save-councils') {
|
||||
const enabled = Boolean(el.querySelector('#assist-councils-enabled')?.checked);
|
||||
const dTier = el.querySelector('#assist-council-d-tier')?.value ?? 'complex';
|
||||
const pTier = el.querySelector('#assist-council-p-tier')?.value ?? 'complex';
|
||||
const metaTier = el.querySelector('#assist-council-meta-tier')?.value ?? 'complex';
|
||||
const dArbiter = (el.querySelector('#assist-council-d-arbiter')?.value ?? '').trim();
|
||||
const dFreethinker = (el.querySelector('#assist-council-d-freethinker')?.value ?? '').trim();
|
||||
const pArbiter = (el.querySelector('#assist-council-p-arbiter')?.value ?? '').trim();
|
||||
const pFreethinker = (el.querySelector('#assist-council-p-freethinker')?.value ?? '').trim();
|
||||
const metaArbiter = (el.querySelector('#assist-council-meta-arbiter')?.value ?? '').trim();
|
||||
const scaffoldPath = (el.querySelector('#assist-council-scaffold')?.value ?? '').trim();
|
||||
const maxRoundsRaw = Number(el.querySelector('#assist-council-max-rounds')?.value ?? 2);
|
||||
const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(6, Math.floor(maxRoundsRaw))) : 2;
|
||||
|
||||
if (!dArbiter || !dFreethinker || !pArbiter || !pFreethinker || !metaArbiter) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'All council agent names are required.';
|
||||
statusEl.className = 'text-sm text-red-500';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
patches = {
|
||||
'councils.enabled': enabled,
|
||||
'councils.defaults.max_rounds': maxRounds,
|
||||
'councils.groups.D.model_tier': dTier,
|
||||
'councils.groups.P.model_tier': pTier,
|
||||
'councils.meta_model_tier': metaTier,
|
||||
'councils.groups.D.arbiter_agent': dArbiter,
|
||||
'councils.groups.D.freethinker_agent': dFreethinker,
|
||||
'councils.groups.P.arbiter_agent': pArbiter,
|
||||
'councils.groups.P.freethinker_agent': pFreethinker,
|
||||
'councils.meta_arbiter_agent': metaArbiter,
|
||||
'councils.scaffold_path': scaffoldPath,
|
||||
};
|
||||
} else if (action === 'run-council') {
|
||||
const councilTask = (el.querySelector('#assist-council-task')?.value ?? '').trim();
|
||||
const councilStatusEl = el.querySelector('#assist-council-status');
|
||||
const ok = await triggerCouncilRun(councilTask, councilStatusEl);
|
||||
if (ok && _lastAssistantConfig) {
|
||||
updateAssistantHealth(_lastAssistantConfig);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!patches) {return;}
|
||||
const patchResult = await applyAssistantPatch(patches, statusEl);
|
||||
|
||||
Reference in New Issue
Block a user