Ship Assistent 0.10: settings panel and UserPrefs with prompt weight.
Separate About-the-user memory (global + per-persona) from craft RAG, add tabbed settings with persona export/import and craft clear APIs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+165
-1
@@ -729,11 +729,172 @@
|
||||
}
|
||||
|
||||
.sa-settings {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 0.7rem 0.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||
background: color-mix(in srgb, currentColor 5%, transparent);
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.sa-settings-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sa-settings-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.sa-stab {
|
||||
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.sa-stab:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sa-stab-active {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
border-color: color-mix(in srgb, currentColor 35%, transparent);
|
||||
}
|
||||
|
||||
.sa-settings-panes {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sa-spane {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.sa-settings-hint {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sa-settings-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sa-settings-row.sa-knob-row label {
|
||||
flex: 1;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.sa-settings-health {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sa-danger-btn {
|
||||
color: #e06c75 !important;
|
||||
}
|
||||
|
||||
.sa-persona-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(8rem, 11rem) 1fr;
|
||||
gap: 0.5rem;
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.sa-persona-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.sa-persona-item {
|
||||
text-align: left;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0.35rem 0.4rem;
|
||||
border-radius: 0.3rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.sa-persona-item:hover,
|
||||
.sa-persona-item-active {
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
.sa-persona-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.sa-persona-dot {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sa-persona-badge {
|
||||
font-size: 0.62rem;
|
||||
opacity: 0.55;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sa-persona-preview {
|
||||
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.sa-user-prefs-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.sa-user-prefs-cols,
|
||||
.sa-persona-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sa-mem-search {
|
||||
flex: 1;
|
||||
min-width: 6rem;
|
||||
max-width: 12rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.sa-settings label {
|
||||
@@ -744,6 +905,9 @@
|
||||
}
|
||||
|
||||
.sa-settings input[type="text"],
|
||||
.sa-settings input[type="number"],
|
||||
.sa-settings input[type="search"],
|
||||
.sa-settings input[type="range"],
|
||||
.sa-select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
+715
-42
@@ -160,6 +160,9 @@
|
||||
slashIndex: 0,
|
||||
llmParked: false,
|
||||
memoryRows: [],
|
||||
userPrefs: [],
|
||||
settingsTab: 'behavior',
|
||||
settingsPersonaId: null,
|
||||
wanted: { count: 0, items: [] },
|
||||
wantedKeys: new Set(),
|
||||
ollamaHealth: 'unknown',
|
||||
@@ -2152,6 +2155,18 @@
|
||||
fillEmptyParamsFromExact();
|
||||
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
|
||||
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
|
||||
const settings = $('sa_settings');
|
||||
if (settings && !settings.hidden) {
|
||||
if (state.settingsTab === 'user') {
|
||||
refreshUserPrefs();
|
||||
}
|
||||
if (state.settingsTab === 'craft') {
|
||||
renderMemoryList();
|
||||
}
|
||||
if (state.settingsTab === 'more') {
|
||||
fillKnobsFromConfig(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2371,7 +2386,7 @@
|
||||
auto_generate: !!$('sa_auto_generate')?.checked,
|
||||
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
|
||||
model_cards: [],
|
||||
taste_profile: summarizeTaste(),
|
||||
user_prefs_count: 0,
|
||||
...initCtx,
|
||||
};
|
||||
|
||||
@@ -2696,6 +2711,34 @@
|
||||
taste.updated = Date.now();
|
||||
state.taste = taste;
|
||||
saveTaste();
|
||||
syncTasteHintsToUserPrefs(taste);
|
||||
}
|
||||
|
||||
function syncTasteHintsToUserPrefs(taste) {
|
||||
if (typeof genericRequest !== 'function' || !taste) {
|
||||
return;
|
||||
}
|
||||
const upsert = (key, text) => {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentUpsertUserPref',
|
||||
{ key, text: String(text).slice(0, 240), scope: 'global', source: 'migrated_taste', pinned: false },
|
||||
() => {},
|
||||
0,
|
||||
() => {},
|
||||
);
|
||||
};
|
||||
if (taste.avoid?.[0]) {
|
||||
upsert('avoid_hint', `Avoid: ${taste.avoid.slice(0, 4).join('; ')}`);
|
||||
}
|
||||
if (taste.styles?.[0]) {
|
||||
upsert('style_hint', `Styles: ${taste.styles.slice(0, 4).join('; ')}`);
|
||||
}
|
||||
if (taste.likes?.[0]) {
|
||||
upsert('like_hint', `Often uses: ${taste.likes.slice(0, 4).join('; ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback key list — only used if assistent.patch.js failed to load.
|
||||
@@ -2703,6 +2746,7 @@
|
||||
'prompt', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler',
|
||||
'actions', 'search_query', 'civitai_query', 'init_creativity', 'denoise',
|
||||
'look_at', 'vision_from', 'vision_slots', 'aspect', 'batch', 'vary', 'lock_seed', 'pack',
|
||||
'memories', 'user_prefs',
|
||||
];
|
||||
|
||||
function isPatchObject(obj) {
|
||||
@@ -4265,6 +4309,7 @@
|
||||
if (asst.context_prompt_max != null) {
|
||||
CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2000);
|
||||
}
|
||||
fillKnobsFromConfig(data);
|
||||
if (applyDefaults || data.exact) {
|
||||
fillEmptyParamsFromExact();
|
||||
}
|
||||
@@ -4537,37 +4582,42 @@
|
||||
|
||||
function setModelOptions(models, { error } = {}) {
|
||||
const sel = $('sa_model');
|
||||
if (!sel) {
|
||||
return;
|
||||
}
|
||||
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
|
||||
sel.innerHTML = '';
|
||||
if (error) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
|
||||
sel.appendChild(opt);
|
||||
sel.disabled = true;
|
||||
return;
|
||||
}
|
||||
sel.disabled = false;
|
||||
if (!names.length) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.textContent = 'No Ollama models — pull / Refresh';
|
||||
sel.appendChild(opt);
|
||||
return;
|
||||
}
|
||||
for (const name of names) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
|
||||
if (prefer && names.includes(prefer)) {
|
||||
sel.value = prefer;
|
||||
}
|
||||
const sel2 = $('sa_settings_chat_model');
|
||||
const apply = (target) => {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
|
||||
target.innerHTML = '';
|
||||
if (error) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
|
||||
target.appendChild(opt);
|
||||
target.disabled = true;
|
||||
return;
|
||||
}
|
||||
target.disabled = false;
|
||||
if (!names.length) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.textContent = 'No Ollama models — pull / Refresh';
|
||||
target.appendChild(opt);
|
||||
return;
|
||||
}
|
||||
for (const name of names) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
target.appendChild(opt);
|
||||
}
|
||||
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
|
||||
if (prefer && names.includes(prefer)) {
|
||||
target.value = prefer;
|
||||
}
|
||||
};
|
||||
apply(sel);
|
||||
apply(sel2);
|
||||
}
|
||||
|
||||
function setEmbedModelOptions(models) {
|
||||
@@ -4691,6 +4741,14 @@
|
||||
return $('sa_mem_kind')?.value || 'all';
|
||||
}
|
||||
|
||||
function memoryScopeFilter() {
|
||||
return $('sa_mem_scope')?.value || 'all';
|
||||
}
|
||||
|
||||
function memorySearchFilter() {
|
||||
return ($('sa_mem_search')?.value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function renderMemoryKinds(kinds) {
|
||||
const sel = $('sa_mem_kind');
|
||||
if (!sel) {
|
||||
@@ -4713,16 +4771,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
function filteredMemoryRows() {
|
||||
const filter = memoryKindFilter();
|
||||
const scope = memoryScopeFilter();
|
||||
const q = memorySearchFilter();
|
||||
const persona = $('sa_persona')?.value || 'neutral';
|
||||
return (state.memoryRows || []).filter((m) => {
|
||||
if (filter !== 'all' && m.kind !== filter) {
|
||||
return false;
|
||||
}
|
||||
if (scope === 'shared' && m.scope !== 'shared') {
|
||||
return false;
|
||||
}
|
||||
if (scope === 'personal' && !(m.scope === 'personal' && (m.persona === persona || !m.persona))) {
|
||||
return false;
|
||||
}
|
||||
if (q) {
|
||||
const hay = `${m.kind || ''} ${m.key || ''} ${m.text || ''}`.toLowerCase();
|
||||
if (!hay.includes(q)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderMemoryList() {
|
||||
const root = $('sa_mem_list');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const filter = memoryKindFilter();
|
||||
const rows = (state.memoryRows || []).filter((m) => filter === 'all' || m.kind === filter);
|
||||
const rows = filteredMemoryRows();
|
||||
root.innerHTML = '';
|
||||
if (!rows.length) {
|
||||
root.innerHTML = '<div class="sa-mem-empty">Память пуста — она наполняется из карточек, seed и патчей <code>memory_upsert</code>.</div>';
|
||||
root.innerHTML = '<div class="sa-mem-empty">Крафт-память пуста — карточки, seed и патчи <code>memory_upsert</code>.</div>';
|
||||
return;
|
||||
}
|
||||
for (const row of rows) {
|
||||
@@ -4800,6 +4882,533 @@
|
||||
);
|
||||
}
|
||||
|
||||
function clearCraftMemory(opts = {}) {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
const label = opts.label || 'крафт-память';
|
||||
if (!window.confirm(`Очистить ${label}? Bundled seed останется.`)) {
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
scope: opts.scope || '',
|
||||
kind: opts.kind || '',
|
||||
persona: opts.persona || '',
|
||||
};
|
||||
genericRequest(
|
||||
'AssistentClearMemory',
|
||||
body,
|
||||
(data) => {
|
||||
setStatus(`Удалено крафт-записей: ${data?.deleted ?? 0}`);
|
||||
refreshMemoryList();
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Очистка не удалась')),
|
||||
);
|
||||
}
|
||||
|
||||
function setSettingsTab(id) {
|
||||
state.settingsTab = id || 'behavior';
|
||||
document.querySelectorAll('#sa_settings .sa-stab').forEach((btn) => {
|
||||
const on = btn.getAttribute('data-stab') === state.settingsTab;
|
||||
btn.classList.toggle('sa-stab-active', on);
|
||||
btn.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
});
|
||||
document.querySelectorAll('#sa_settings .sa-spane').forEach((pane) => {
|
||||
pane.hidden = pane.getAttribute('data-spane') !== state.settingsTab;
|
||||
});
|
||||
if (state.settingsTab === 'craft') {
|
||||
refreshMemoryList();
|
||||
refreshWantedQueue();
|
||||
}
|
||||
if (state.settingsTab === 'user') {
|
||||
refreshUserPrefs();
|
||||
}
|
||||
if (state.settingsTab === 'personas') {
|
||||
renderPersonaSettingsList();
|
||||
}
|
||||
if (state.settingsTab === 'models') {
|
||||
syncSettingsHealthLine();
|
||||
const m = $('sa_model')?.value;
|
||||
if (m && $('sa_settings_chat_model')) {
|
||||
$('sa_settings_chat_model').value = m;
|
||||
}
|
||||
}
|
||||
if (state.settingsTab === 'more') {
|
||||
fillKnobsFromConfig(state.config);
|
||||
}
|
||||
}
|
||||
|
||||
function openSettings(tab) {
|
||||
const s = $('sa_settings');
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
s.hidden = false;
|
||||
setSettingsTab(tab || state.settingsTab || 'behavior');
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
const s = $('sa_settings');
|
||||
if (s) {
|
||||
s.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function fillKnobsFromConfig(data) {
|
||||
const asst = data?.assistant || state.config?.assistant || {};
|
||||
const exact = data?.exact || state.config?.exact || state.exact || {};
|
||||
const setNum = (id, v) => {
|
||||
const el = $(id);
|
||||
if (el && v != null && Number.isFinite(Number(v))) {
|
||||
el.value = String(v);
|
||||
}
|
||||
};
|
||||
setNum('sa_num_ctx', asst.num_ctx);
|
||||
setNum('sa_history_keep', asst.history_keep_turns);
|
||||
setNum('sa_memory_top_k', asst.memory_top_k);
|
||||
const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1;
|
||||
const weightEl = $('sa_user_prefs_weight');
|
||||
if (weightEl) {
|
||||
weightEl.value = String(Math.max(0, Math.min(1.5, w)));
|
||||
const lab = $('sa_user_prefs_weight_val');
|
||||
if (lab) {
|
||||
lab.textContent = Number(weightEl.value).toFixed(1);
|
||||
}
|
||||
}
|
||||
const turbo = exact.profiles?.turbo || {};
|
||||
const raw = exact.profiles?.raw || {};
|
||||
setNum('sa_exact_turbo_steps', turbo.steps);
|
||||
setNum('sa_exact_turbo_cfg', turbo.cfg);
|
||||
setNum('sa_exact_turbo_sigma', turbo.sigma_shift);
|
||||
setNum('sa_exact_raw_steps', raw.steps);
|
||||
setNum('sa_exact_raw_cfg', raw.cfg);
|
||||
setNum('sa_exact_raw_sigma', raw.sigma_shift);
|
||||
}
|
||||
|
||||
function saveKnobs() {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
const num = (id) => {
|
||||
const v = parseFloat($(id)?.value);
|
||||
return Number.isFinite(v) ? v : null;
|
||||
};
|
||||
const assistant = {
|
||||
num_ctx: num('sa_num_ctx'),
|
||||
history_keep_turns: num('sa_history_keep'),
|
||||
memory_top_k: num('sa_memory_top_k'),
|
||||
user_prefs_weight: num('sa_user_prefs_weight'),
|
||||
};
|
||||
Object.keys(assistant).forEach((k) => {
|
||||
if (assistant[k] == null) {
|
||||
delete assistant[k];
|
||||
}
|
||||
});
|
||||
const exact = {
|
||||
profiles: {
|
||||
turbo: {
|
||||
steps: num('sa_exact_turbo_steps'),
|
||||
cfg: num('sa_exact_turbo_cfg'),
|
||||
sigma_shift: num('sa_exact_turbo_sigma'),
|
||||
},
|
||||
raw: {
|
||||
steps: num('sa_exact_raw_steps'),
|
||||
cfg: num('sa_exact_raw_cfg'),
|
||||
sigma_shift: num('sa_exact_raw_sigma'),
|
||||
},
|
||||
},
|
||||
};
|
||||
genericRequest(
|
||||
'AssistentSaveKnobs',
|
||||
{ assistant, exact },
|
||||
(data) => {
|
||||
if (data?.assistant || data?.exact) {
|
||||
applyConfigPayload({
|
||||
...state.config,
|
||||
assistant: data.assistant || state.config?.assistant,
|
||||
exact: data.exact || state.config?.exact,
|
||||
});
|
||||
}
|
||||
setStatus('Knobs сохранены в overlay');
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Не удалось сохранить knobs')),
|
||||
);
|
||||
}
|
||||
|
||||
function syncSettingsHealthLine() {
|
||||
const line = $('sa_settings_health_line');
|
||||
const badge = $('sa_ollama_health');
|
||||
if (line && badge) {
|
||||
line.textContent = badge.textContent || 'Ollama · …';
|
||||
line.className = 'sa-settings-health ' + (badge.className || '').replace('sa-health', '').trim();
|
||||
}
|
||||
}
|
||||
|
||||
function personaSourceLabel(source) {
|
||||
if (source === 'overlay') {
|
||||
return 'моя';
|
||||
}
|
||||
if (source === 'overlay+bundled') {
|
||||
return 'встроено+правка';
|
||||
}
|
||||
return 'встроено';
|
||||
}
|
||||
|
||||
function renderPersonaSettingsList() {
|
||||
const root = $('sa_persona_list');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const list = state.personas || state.config?.personas || [];
|
||||
const cur = state.settingsPersonaId || $('sa_persona')?.value || list[0]?.id;
|
||||
state.settingsPersonaId = cur;
|
||||
root.innerHTML = '';
|
||||
for (const p of list) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'sa-persona-item' + (p.id === cur ? ' sa-persona-item-active' : '');
|
||||
const accent = p.accent || 'currentColor';
|
||||
btn.innerHTML = `<div class="sa-persona-item-title"><span class="sa-persona-dot" style="background:${escapeHtml(accent)}"></span><span>${escapeHtml(p.title || p.id)}</span></div><div class="sa-persona-badge">${escapeHtml(personaSourceLabel(p.source))}</div>`;
|
||||
btn.addEventListener('click', () => {
|
||||
state.settingsPersonaId = p.id;
|
||||
renderPersonaSettingsList();
|
||||
loadPersonaPreview(p.id);
|
||||
});
|
||||
root.appendChild(btn);
|
||||
}
|
||||
syncPersonaPanelActions();
|
||||
if (cur) {
|
||||
loadPersonaPreview(cur);
|
||||
}
|
||||
}
|
||||
|
||||
function syncPersonaPanelActions() {
|
||||
const id = state.settingsPersonaId;
|
||||
const p = (state.personas || []).find((x) => x.id === id);
|
||||
const canDelete = p && (p.source === 'overlay' || p.source === 'overlay+bundled');
|
||||
const del = $('sa_btn_persona_delete_panel');
|
||||
if (del) {
|
||||
del.disabled = !canDelete;
|
||||
}
|
||||
}
|
||||
|
||||
function loadPersonaPreview(id) {
|
||||
const box = $('sa_persona_preview');
|
||||
if (!box || typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
box.innerHTML = '<div class="sa-mem-empty">Загрузка…</div>';
|
||||
genericRequest(
|
||||
'AssistentGetPersonaShelves',
|
||||
{ persona: id },
|
||||
(data) => {
|
||||
const summary = data?.identity_summary || '';
|
||||
const src = data?.source || '';
|
||||
box.textContent = `${id} · ${personaSourceLabel(src)}\n\n${summary || '(пусто)'}`;
|
||||
syncPersonaPanelActions();
|
||||
},
|
||||
0,
|
||||
(err) => {
|
||||
box.innerHTML = `<div class="sa-mem-empty">${escapeHtml(String(err || 'ошибка'))}</div>`;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function exportSelectedPersona() {
|
||||
const id = state.settingsPersonaId || $('sa_persona')?.value;
|
||||
if (!id || typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentExportPersona',
|
||||
{ persona: id },
|
||||
(data) => {
|
||||
const pack = data?.pack;
|
||||
if (!pack) {
|
||||
setStatus('Пустой экспорт');
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(pack, null, 2)], { type: 'application/json' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${pack.id || id}.assistent-persona.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
setStatus(`Экспорт: ${a.download}`);
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Экспорт не удался')),
|
||||
);
|
||||
}
|
||||
|
||||
function importPersonaFile(file) {
|
||||
if (!file || typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
let pack;
|
||||
try {
|
||||
pack = JSON.parse(String(reader.result || ''));
|
||||
} catch (e) {
|
||||
setStatus('Невалидный JSON');
|
||||
return;
|
||||
}
|
||||
let newId = pack?.id || '';
|
||||
if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'overlay+bundled'))) {
|
||||
newId = window.prompt('Id занят bundled — новый id:', `${newId}_import`) || '';
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentImportPersona',
|
||||
{ pack, new_id: newId || null, overwrite: false },
|
||||
(data) => {
|
||||
if (Array.isArray(data?.personas)) {
|
||||
state.personas = data.personas;
|
||||
renderPersonaOptions(data.personas, data.persona?.id);
|
||||
}
|
||||
state.settingsPersonaId = data?.persona?.id || newId;
|
||||
renderPersonaSettingsList();
|
||||
setStatus(`Импорт: ${data?.persona?.id || newId}`);
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Импорт не удался')),
|
||||
);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function cloneSelectedPersona() {
|
||||
const from = state.settingsPersonaId || $('sa_persona')?.value;
|
||||
if (!from) {
|
||||
return;
|
||||
}
|
||||
const to = window.prompt('Новый id личности:', `${from}_copy`);
|
||||
if (!to) {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentClonePersona',
|
||||
{ from, to, title: to },
|
||||
(data) => {
|
||||
if (Array.isArray(data?.personas)) {
|
||||
state.personas = data.personas;
|
||||
renderPersonaOptions(data.personas, to);
|
||||
}
|
||||
state.settingsPersonaId = to;
|
||||
renderPersonaSettingsList();
|
||||
setStatus(`Клон: ${to}`);
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Клон не удался')),
|
||||
);
|
||||
}
|
||||
|
||||
function deleteSelectedOverlayPersona() {
|
||||
const id = state.settingsPersonaId;
|
||||
const p = (state.personas || []).find((x) => x.id === id);
|
||||
if (!p || (p.source !== 'overlay' && p.source !== 'overlay+bundled')) {
|
||||
setStatus('Можно удалить только overlay');
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`Удалить overlay-личность «${id}»?`)) {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentDeletePersona',
|
||||
{ persona: id },
|
||||
(data) => {
|
||||
if (Array.isArray(data?.personas)) {
|
||||
state.personas = data.personas;
|
||||
renderPersonaOptions(data.personas, data.default_persona);
|
||||
}
|
||||
state.settingsPersonaId = data?.default_persona || null;
|
||||
renderPersonaSettingsList();
|
||||
setStatus(`Удалено: ${id}`);
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Удаление не удалось')),
|
||||
);
|
||||
}
|
||||
|
||||
function refreshUserPrefs() {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
const persona = $('sa_persona')?.value || 'neutral';
|
||||
genericRequest(
|
||||
'AssistentListUserPrefs',
|
||||
{ persona, limit: 200 },
|
||||
(data) => {
|
||||
state.userPrefs = Array.isArray(data?.prefs) ? data.prefs : [];
|
||||
renderUserPrefsLists();
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'User prefs недоступны')),
|
||||
);
|
||||
}
|
||||
|
||||
function renderUserPrefsLists() {
|
||||
const persona = $('sa_persona')?.value || 'neutral';
|
||||
const global = (state.userPrefs || []).filter((p) => p.scope === 'global');
|
||||
const personal = (state.userPrefs || []).filter((p) => p.scope === 'persona' && (p.persona_id === persona || p.persona === persona));
|
||||
const fill = (rootId, rows) => {
|
||||
const root = $(rootId);
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
root.innerHTML = '';
|
||||
if (!rows.length) {
|
||||
root.innerHTML = '<div class="sa-mem-empty">Пусто</div>';
|
||||
return;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'sa-mem-row';
|
||||
const pin = row.pinned ? ' ★' : '';
|
||||
el.innerHTML = `<div class="sa-mem-row-body"><div class="sa-mem-row-head"><span class="sa-mem-row-key">${escapeHtml(row.key || '')}${pin}</span></div><div class="sa-mem-row-text">${escapeHtml(clipDebug(row.text, 200))}</div></div>`;
|
||||
el.querySelector('.sa-mem-row-body')?.addEventListener('click', () => editUserPref(row));
|
||||
el.querySelector('.sa-mem-row-body')?.setAttribute('title', 'Клик — редактировать');
|
||||
const pinBtn = document.createElement('button');
|
||||
pinBtn.type = 'button';
|
||||
pinBtn.className = 'basic-button sa-mem-forget';
|
||||
pinBtn.textContent = row.pinned ? '★' : '☆';
|
||||
pinBtn.title = row.pinned ? 'Unpin' : 'Pin';
|
||||
pinBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleUserPrefPin(row);
|
||||
});
|
||||
el.appendChild(pinBtn);
|
||||
const forget = document.createElement('button');
|
||||
forget.type = 'button';
|
||||
forget.className = 'basic-button sa-mem-forget';
|
||||
forget.textContent = '×';
|
||||
forget.title = 'Забыть';
|
||||
forget.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
forgetUserPref(row);
|
||||
});
|
||||
el.appendChild(forget);
|
||||
root.appendChild(el);
|
||||
}
|
||||
};
|
||||
fill('sa_prefs_global', global);
|
||||
fill('sa_prefs_persona', personal);
|
||||
}
|
||||
|
||||
function editUserPref(row) {
|
||||
const text = window.prompt('Текст факта:', row.text || '');
|
||||
if (text == null || !String(text).trim()) {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentUpsertUserPref',
|
||||
{
|
||||
key: row.key,
|
||||
text: String(text).trim(),
|
||||
scope: row.scope || 'global',
|
||||
persona: row.persona_id || row.persona || $('sa_persona')?.value || 'neutral',
|
||||
source: 'user',
|
||||
pinned: !!row.pinned,
|
||||
},
|
||||
() => refreshUserPrefs(),
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Не удалось сохранить')),
|
||||
);
|
||||
}
|
||||
|
||||
function toggleUserPrefPin(row) {
|
||||
genericRequest(
|
||||
'AssistentUpsertUserPref',
|
||||
{
|
||||
key: row.key,
|
||||
text: row.text,
|
||||
scope: row.scope || 'global',
|
||||
persona: row.persona_id || row.persona || $('sa_persona')?.value || 'neutral',
|
||||
source: row.source || 'user',
|
||||
pinned: !row.pinned,
|
||||
},
|
||||
() => refreshUserPrefs(),
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Не удалось pin')),
|
||||
);
|
||||
}
|
||||
|
||||
function addUserPref(scope) {
|
||||
const key = window.prompt('Ключ (stable-id):', scope === 'global' ? 'prefer' : 'tone');
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const text = window.prompt('Текст факта:', '');
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentUpsertUserPref',
|
||||
{
|
||||
key: key.trim(),
|
||||
text: text.trim(),
|
||||
scope,
|
||||
persona: $('sa_persona')?.value || 'neutral',
|
||||
source: 'user',
|
||||
pinned: false,
|
||||
},
|
||||
() => {
|
||||
refreshUserPrefs();
|
||||
setStatus('Сохранено');
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Не удалось сохранить')),
|
||||
);
|
||||
}
|
||||
|
||||
function forgetUserPref(row) {
|
||||
genericRequest(
|
||||
'AssistentForgetUserPref',
|
||||
{
|
||||
key: row.key,
|
||||
scope: row.scope || 'global',
|
||||
persona: row.persona_id || row.persona || $('sa_persona')?.value,
|
||||
},
|
||||
() => refreshUserPrefs(),
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Не удалось забыть')),
|
||||
);
|
||||
}
|
||||
|
||||
function clearUserPrefs(scope) {
|
||||
const labels = { global: 'общие prefs', persona: 'prefs этой личности', all: 'все prefs о пользователе' };
|
||||
if (!window.confirm(`Очистить ${labels[scope] || scope}?`)) {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentClearUserPrefs',
|
||||
{ scope, persona: $('sa_persona')?.value || 'neutral' },
|
||||
(data) => {
|
||||
setStatus(`Удалено: ${data?.deleted ?? 0}`);
|
||||
refreshUserPrefs();
|
||||
},
|
||||
0,
|
||||
(err) => setStatus(String(err || 'Очистка не удалась')),
|
||||
);
|
||||
}
|
||||
|
||||
function resetUiState() {
|
||||
if (!window.confirm('Сбросить UI-state (local + disk)? Настройки Ollama и prefs останутся.')) {
|
||||
return;
|
||||
}
|
||||
const keys = Object.keys(localStorage).filter((k) => k.startsWith('swarm_assistent_'));
|
||||
for (const k of keys) {
|
||||
if (k === LS_TASTE) {
|
||||
continue;
|
||||
}
|
||||
localStorage.removeItem(k);
|
||||
}
|
||||
diskPersist()?.saveUiState?.({});
|
||||
setStatus('UI-state сброшен — обнови страницу');
|
||||
}
|
||||
|
||||
function modelKeyLeaf(name) {
|
||||
return String(name || '')
|
||||
.replace(/\\/g, '/')
|
||||
@@ -4877,6 +5486,7 @@
|
||||
el.title = title || text;
|
||||
el.classList.remove('sa-health-ok', 'sa-health-warn', 'sa-health-down');
|
||||
el.classList.add(`sa-health-${level}`);
|
||||
syncSettingsHealthLine();
|
||||
}
|
||||
|
||||
function probeOllamaHealth() {
|
||||
@@ -5871,7 +6481,7 @@
|
||||
} else {
|
||||
why.push('последнего патча Assistent ещё нет');
|
||||
}
|
||||
why.push('приоритет: user → session_exact → exact(+persona) → live UI → memory_hits (shared+personal)');
|
||||
why.push('приоритет: user → About the user → session_exact → exact(+persona) → live UI → craft memory_hits');
|
||||
|
||||
const lines = [
|
||||
'### Debug Assistent',
|
||||
@@ -6725,18 +7335,75 @@
|
||||
$('sa_btn_settings')?.addEventListener('click', () => {
|
||||
const s = $('sa_settings');
|
||||
if (s) {
|
||||
s.hidden = !s.hidden;
|
||||
if (!s.hidden) {
|
||||
refreshMemoryList();
|
||||
refreshWantedQueue();
|
||||
if (s.hidden) {
|
||||
openSettings(state.settingsTab || 'behavior');
|
||||
} else {
|
||||
closeSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
$('sa_settings_close')?.addEventListener('click', () => closeSettings());
|
||||
document.querySelectorAll('#sa_settings .sa-stab').forEach((btn) => {
|
||||
btn.addEventListener('click', () => setSettingsTab(btn.getAttribute('data-stab')));
|
||||
});
|
||||
$('sa_btn_mem_refresh')?.addEventListener('click', () => {
|
||||
refreshMemoryList();
|
||||
refreshWantedQueue();
|
||||
});
|
||||
$('sa_mem_kind')?.addEventListener('change', renderMemoryList);
|
||||
$('sa_mem_scope')?.addEventListener('change', renderMemoryList);
|
||||
$('sa_mem_search')?.addEventListener('input', () => renderMemoryList());
|
||||
$('sa_btn_mem_clear_kind')?.addEventListener('click', () => {
|
||||
const kind = memoryKindFilter();
|
||||
clearCraftMemory({ kind: kind === 'all' ? '' : kind, label: kind === 'all' ? 'весь крафт (фильтр типа)' : `тип ${kind}` });
|
||||
});
|
||||
$('sa_btn_mem_clear_persona')?.addEventListener('click', () => {
|
||||
clearCraftMemory({ scope: 'personal', persona: $('sa_persona')?.value || 'neutral', label: 'крафт этой личности' });
|
||||
});
|
||||
$('sa_btn_mem_clear_shared')?.addEventListener('click', () => {
|
||||
clearCraftMemory({ scope: 'shared', label: 'общую крафт-память' });
|
||||
});
|
||||
$('sa_btn_mem_clear_all')?.addEventListener('click', () => {
|
||||
clearCraftMemory({ label: 'весь крафт (non-bundled)' });
|
||||
});
|
||||
$('sa_btn_prefs_refresh')?.addEventListener('click', () => refreshUserPrefs());
|
||||
$('sa_btn_pref_add_global')?.addEventListener('click', () => addUserPref('global'));
|
||||
$('sa_btn_pref_add_persona')?.addEventListener('click', () => addUserPref('persona'));
|
||||
$('sa_btn_prefs_clear_global')?.addEventListener('click', () => clearUserPrefs('global'));
|
||||
$('sa_btn_prefs_clear_persona')?.addEventListener('click', () => clearUserPrefs('persona'));
|
||||
$('sa_btn_prefs_clear_all')?.addEventListener('click', () => clearUserPrefs('all'));
|
||||
$('sa_user_prefs_weight')?.addEventListener('input', () => {
|
||||
const lab = $('sa_user_prefs_weight_val');
|
||||
if (lab) {
|
||||
lab.textContent = Number($('sa_user_prefs_weight').value).toFixed(1);
|
||||
}
|
||||
});
|
||||
$('sa_user_prefs_weight')?.addEventListener('change', () => saveKnobs());
|
||||
$('sa_memory_top_k')?.addEventListener('change', () => saveKnobs());
|
||||
$('sa_btn_knobs_save')?.addEventListener('click', () => saveKnobs());
|
||||
$('sa_btn_reset_ui')?.addEventListener('click', () => resetUiState());
|
||||
$('sa_btn_persona_export')?.addEventListener('click', () => exportSelectedPersona());
|
||||
$('sa_btn_persona_import')?.addEventListener('click', () => $('sa_persona_import_file')?.click());
|
||||
$('sa_persona_import_file')?.addEventListener('change', (e) => {
|
||||
const file = e.target?.files?.[0];
|
||||
if (file) {
|
||||
importPersonaFile(file);
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
$('sa_btn_persona_clone')?.addEventListener('click', () => cloneSelectedPersona());
|
||||
$('sa_btn_persona_delete_panel')?.addEventListener('click', () => deleteSelectedOverlayPersona());
|
||||
$('sa_btn_settings_health')?.addEventListener('click', () => {
|
||||
probeOllamaHealth();
|
||||
setTimeout(syncSettingsHealthLine, 400);
|
||||
});
|
||||
$('sa_settings_chat_model')?.addEventListener('change', () => {
|
||||
const v = $('sa_settings_chat_model')?.value;
|
||||
if (v && $('sa_model')) {
|
||||
$('sa_model').value = v;
|
||||
saveSettings();
|
||||
}
|
||||
});
|
||||
$('sa_btn_look_result')?.addEventListener('click', () => askLookAtResult());
|
||||
$('sa_ollama_health')?.addEventListener('click', () => probeOllamaHealth());
|
||||
document.addEventListener('keydown', (e) => {
|
||||
@@ -6746,7 +7413,7 @@
|
||||
let closed = false;
|
||||
const settings = $('sa_settings');
|
||||
if (settings && !settings.hidden) {
|
||||
settings.hidden = true;
|
||||
closeSettings();
|
||||
closed = true;
|
||||
}
|
||||
if (state.chatsPanelOpen) {
|
||||
@@ -6854,7 +7521,13 @@
|
||||
$('sa_board_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
|
||||
$('sa_clear_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
|
||||
$('sa_base_url')?.addEventListener('change', saveSettings);
|
||||
$('sa_model')?.addEventListener('change', saveSettings);
|
||||
$('sa_model')?.addEventListener('change', () => {
|
||||
const v = $('sa_model')?.value;
|
||||
if (v && $('sa_settings_chat_model')) {
|
||||
$('sa_settings_chat_model').value = v;
|
||||
}
|
||||
saveSettings();
|
||||
});
|
||||
$('sa_embed_model')?.addEventListener('change', () => {
|
||||
state.preferredEmbed = $('sa_embed_model')?.value || '';
|
||||
saveSettings();
|
||||
|
||||
@@ -14,6 +14,8 @@ window.SA = window.SA || {};
|
||||
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
||||
'creativity', 'intensity', 'complexity', 'movement',
|
||||
'clear_prompt_images', 'slot_to_prompt_image', 'pack',
|
||||
'memories', 'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
|
||||
'controls', 'persona_clone', 'persona_shelves', 'persona', 'notes',
|
||||
];
|
||||
|
||||
const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
|
||||
@@ -30,6 +30,26 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
}
|
||||
|
||||
if (Memory is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject asst = Config.LoadAssistant(pid);
|
||||
double weight = asst["user_prefs_weight"]?.Value<double?>() ?? 1.0;
|
||||
int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 16;
|
||||
string about = Memory.FormatUserPrefsBlock(pid, weight, maxPrefs);
|
||||
if (!string.IsNullOrWhiteSpace(about))
|
||||
{
|
||||
system.AppendLine();
|
||||
system.AppendLine(about);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"BuildOllamaMessages user prefs: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
JObject exact = Config.LoadExactForPrompt(pid);
|
||||
if (exact is not null && exact.Count > 0)
|
||||
{
|
||||
@@ -170,6 +190,7 @@ public partial class SwarmAssistentExtension
|
||||
(reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid);
|
||||
JObject patch = TryParsePatch(reply);
|
||||
await ApplyMemoryActions(root, patch, embed, pid);
|
||||
ApplyUserPrefActions(patch, pid);
|
||||
ApplyPersonaActions(patch, ref pid);
|
||||
if (hop + 1 >= maxHops)
|
||||
{
|
||||
@@ -382,6 +403,19 @@ public partial class SwarmAssistentExtension
|
||||
ctx = new JObject { ["_raw_context"] = contextJson };
|
||||
}
|
||||
ctx["memory_hits"] = hits ?? new JArray();
|
||||
ctx.Remove("taste_profile");
|
||||
try
|
||||
{
|
||||
string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral";
|
||||
JObject asst = Config?.LoadAssistant(pid) ?? new JObject();
|
||||
double weight = asst["user_prefs_weight"]?.Value<double?>() ?? 1.0;
|
||||
int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 16;
|
||||
ctx["user_prefs_count"] = Memory?.SelectUserPrefsForPrompt(pid, weight, maxPrefs).Count ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx["user_prefs_count"] = 0;
|
||||
}
|
||||
// Never re-inject full Exact into live context (already in system prompt).
|
||||
ctx.Remove("exact");
|
||||
if (ctx["session_exact"] is null)
|
||||
@@ -624,4 +658,60 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyUserPrefActions(JObject patch, string personaId)
|
||||
{
|
||||
if (patch is null || Memory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
bool upsert = false, forget = false;
|
||||
if (patch["actions"] is JArray acts)
|
||||
{
|
||||
foreach (JToken a in acts)
|
||||
{
|
||||
string s = a?.ToString() ?? "";
|
||||
if (string.Equals(s, "user_pref_upsert", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
upsert = true;
|
||||
}
|
||||
if (string.Equals(s, "user_pref_forget", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
forget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
JArray prefs = patch["user_prefs"] as JArray;
|
||||
if (prefs is null || prefs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
|
||||
foreach (JToken t in prefs)
|
||||
{
|
||||
if (t is not JObject mo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string key = mo["key"]?.ToString() ?? "";
|
||||
string text = mo["text"]?.ToString() ?? "";
|
||||
string scope = mo["scope"]?.ToString() ?? "global";
|
||||
bool pinned = mo["pinned"]?.Value<bool>() == true;
|
||||
try
|
||||
{
|
||||
if (forget && string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
Memory.ForgetUserPref(key, scope, pid);
|
||||
}
|
||||
else if (upsert || !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
Memory.UpsertUserPref(key, text, scope, pid, "agent", pinned);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"ApplyUserPrefActions: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+259
-11
@@ -715,6 +715,145 @@ public sealed class AssistentConfig
|
||||
/// <summary>Exact (KV) memory: bundled base → persona overlays → disk overlays. Persona keys overwrite base.</summary>
|
||||
public JObject LoadExact(string personaId) => MergeJsonLayers("exact.json", LayerRoots(personaId));
|
||||
|
||||
/// <summary>DeepMerge sparse keys into Assistent/_base/<fileName> (disk overlay only).</summary>
|
||||
public JObject MergeOverlayBaseJson(string fileName, JObject sparse)
|
||||
{
|
||||
string safe = Path.GetFileName(fileName ?? "");
|
||||
if (string.IsNullOrWhiteSpace(safe) || !safe.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("invalid overlay base json name");
|
||||
}
|
||||
string dir = Path.Combine(_overlayRoot, "_base");
|
||||
Directory.CreateDirectory(dir);
|
||||
string path = Path.Combine(dir, safe);
|
||||
JObject existing = TryReadJson(path) ?? new JObject();
|
||||
JObject merged = DeepMerge(existing, sparse ?? new JObject());
|
||||
File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/// <summary>Export a shareable persona pack (merged shelves + controls + personal memory-seed).</summary>
|
||||
public JObject ExportPersonaPack(string personaId)
|
||||
{
|
||||
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
|
||||
JObject shelves = LoadIdentityParts(id);
|
||||
JObject controls = LoadControlsSchema(id);
|
||||
JObject controlValues = LoadControlValues(id);
|
||||
JArray seed = [];
|
||||
foreach (JObject doc in LoadMemorySeedDocs())
|
||||
{
|
||||
string persona = AssistentMemory.NormalizePersona(doc["persona"]?.ToString());
|
||||
if (!string.Equals(persona, id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string kind = doc["kind"]?.ToString() ?? "note";
|
||||
string key = doc["key"]?.ToString() ?? "";
|
||||
string text = doc["text"]?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
seed.Add(new JObject
|
||||
{
|
||||
["kind"] = kind,
|
||||
["key"] = key,
|
||||
["text"] = text,
|
||||
});
|
||||
}
|
||||
JObject metaShelf = shelves["persona"] as JObject ?? new JObject();
|
||||
return new JObject
|
||||
{
|
||||
["format"] = "swarm-assistent-persona",
|
||||
["version"] = 1,
|
||||
["id"] = id,
|
||||
["title"] = metaShelf["title"]?.ToString() ?? id,
|
||||
["shelves"] = shelves,
|
||||
["exact_controls"] = controlValues,
|
||||
["controls_schema"] = controls,
|
||||
["memory_seed"] = seed,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Import pack into overlay personas/<id>. Never overwrites bundled files.</summary>
|
||||
public JObject ImportPersonaPack(JObject pack, string newId = null, bool overwriteOverlay = false)
|
||||
{
|
||||
if (pack is null || !string.Equals(pack["format"]?.ToString(), "swarm-assistent-persona", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("invalid persona pack format");
|
||||
}
|
||||
string requested = SafeId(newId) ?? SafeId(pack["id"]?.ToString());
|
||||
if (requested is null)
|
||||
{
|
||||
throw new ArgumentException("invalid persona id");
|
||||
}
|
||||
string id = requested;
|
||||
if (IsBundledPersona(id) && !IsOverlayPersona(id))
|
||||
{
|
||||
// Force rename away from bundled id
|
||||
id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over bundled id — pick a new id");
|
||||
if (IsBundledPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay))
|
||||
{
|
||||
throw new InvalidOperationException($"id '{requested}' is bundled — pass new_id");
|
||||
}
|
||||
}
|
||||
if (IsOverlayPersona(id) && !overwriteOverlay)
|
||||
{
|
||||
throw new InvalidOperationException($"overlay persona '{id}' already exists — pass overwrite or new_id");
|
||||
}
|
||||
string dest = Path.Combine(_overlayRoot, "personas", id);
|
||||
Directory.CreateDirectory(dest);
|
||||
if (pack["shelves"] is JObject shelves)
|
||||
{
|
||||
foreach (JProperty prop in shelves.Properties())
|
||||
{
|
||||
string fileName = prop.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|
||||
? Path.GetFileName(prop.Name)
|
||||
: prop.Name + ".json";
|
||||
if (!IsWritableShelfName(fileName) && !string.Equals(fileName, "persona.json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (prop.Value is JObject jo)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, fileName),
|
||||
jo.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
string personaPath = Path.Combine(dest, "persona.json");
|
||||
JObject meta = TryReadJson(personaPath) ?? new JObject();
|
||||
meta["id"] = id;
|
||||
if (!string.IsNullOrWhiteSpace(pack["title"]?.ToString()))
|
||||
{
|
||||
meta["title"] = pack["title"]?.ToString();
|
||||
}
|
||||
File.WriteAllText(personaPath, meta.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
if (pack["controls_schema"] is JObject schema && schema.Count > 0)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, "controls.json"),
|
||||
schema.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
if (pack["exact_controls"] is JObject ctrl && ctrl.Count > 0)
|
||||
{
|
||||
File.WriteAllText(Path.Combine(dest, "exact.json"),
|
||||
new JObject { ["controls"] = ctrl }.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
if (pack["memory_seed"] is JArray seedArr && seedArr.Count > 0)
|
||||
{
|
||||
string seedDir = Path.Combine(dest, "memory-seed");
|
||||
Directory.CreateDirectory(seedDir);
|
||||
File.WriteAllText(Path.Combine(seedDir, "imported.json"),
|
||||
seedArr.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||
}
|
||||
return new JObject
|
||||
{
|
||||
["id"] = id,
|
||||
["title"] = meta["title"]?.ToString() ?? id,
|
||||
["source"] = "overlay",
|
||||
};
|
||||
}
|
||||
|
||||
public JObject LoadModelProfile(string personaId)
|
||||
{
|
||||
JObject assistant = LoadAssistant(personaId);
|
||||
@@ -962,7 +1101,37 @@ public sealed class AssistentConfig
|
||||
return char.ToUpperInvariant(key[0]) + key[1..];
|
||||
}
|
||||
|
||||
public string RenderIdentityBlock(string personaId)
|
||||
static readonly string[] DefaultIdentityAlwaysShelves =
|
||||
["persona", "voice", "rules", "likes", "dislikes"];
|
||||
|
||||
/// <summary>Shelves always injected into the system prompt. Lore shelves load via persona_read hop.</summary>
|
||||
public HashSet<string> IdentityAlwaysShelves(string personaId)
|
||||
{
|
||||
HashSet<string> set = new(StringComparer.OrdinalIgnoreCase);
|
||||
JObject asst = LoadAssistant(SafeId(personaId) ?? "neutral") ?? new JObject();
|
||||
if (asst["identity_always_shelves"] is JArray arr && arr.Count > 0)
|
||||
{
|
||||
foreach (JToken t in arr)
|
||||
{
|
||||
string name = SafeId(t?.ToString()) ?? t?.ToString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
set.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (set.Count == 0)
|
||||
{
|
||||
foreach (string s in DefaultIdentityAlwaysShelves)
|
||||
{
|
||||
set.Add(s);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>Render always-on identity (voice/taste). Pass includeAllShelves for author pack / persona_read.</summary>
|
||||
public string RenderIdentityBlock(string personaId, bool includeAllShelves = false, IEnumerable<string> onlyShelves = null)
|
||||
{
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
JObject parts = LoadIdentityParts(id);
|
||||
@@ -976,12 +1145,26 @@ public sealed class AssistentConfig
|
||||
sb.AppendLine($"*{tagline}*");
|
||||
}
|
||||
|
||||
HashSet<string> allow = null;
|
||||
if (onlyShelves is not null)
|
||||
{
|
||||
allow = new HashSet<string>(onlyShelves.Where(s => !string.IsNullOrWhiteSpace(s)), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (!includeAllShelves)
|
||||
{
|
||||
allow = IdentityAlwaysShelves(id);
|
||||
}
|
||||
|
||||
foreach (JProperty prop in parts.Properties())
|
||||
{
|
||||
if (prop.Name is "persona" or "extra")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (allow is not null && !allow.Contains(prop.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (prop.Value is not JObject shelf || !shelf.Properties().Any())
|
||||
{
|
||||
continue;
|
||||
@@ -991,22 +1174,87 @@ public sealed class AssistentConfig
|
||||
AppendTokenMarkdown(sb, shelf, 0);
|
||||
}
|
||||
|
||||
string controlsBlock = RenderControlsBlock(id);
|
||||
if (!string.IsNullOrWhiteSpace(controlsBlock))
|
||||
// Controls schema/meanings are fat; Exact already carries current values. Only author/full dump.
|
||||
if (includeAllShelves && onlyShelves is null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(controlsBlock);
|
||||
}
|
||||
string controlsBlock = RenderControlsBlock(id);
|
||||
if (!string.IsNullOrWhiteSpace(controlsBlock))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(controlsBlock);
|
||||
}
|
||||
|
||||
string extra = parts["extra"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(extra))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(extra.Trim());
|
||||
string extra = parts["extra"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(extra))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(extra.Trim());
|
||||
}
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>Markdown for lore shelves not in always-on (persona_read hop).</summary>
|
||||
public string RenderPersonaReadBlock(string personaId, IEnumerable<string> shelfNames)
|
||||
{
|
||||
string id = SafeId(personaId) ?? "neutral";
|
||||
HashSet<string> always = IdentityAlwaysShelves(id);
|
||||
List<string> want = [];
|
||||
if (shelfNames is not null)
|
||||
{
|
||||
foreach (string raw in shelfNames)
|
||||
{
|
||||
string name = SafeId(raw) ?? raw?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(name) && !always.Contains(name) && !string.Equals(name, "persona", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
want.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (want.Count == 0)
|
||||
{
|
||||
JObject parts = LoadIdentityParts(id);
|
||||
foreach (JProperty prop in parts.Properties())
|
||||
{
|
||||
if (prop.Name is "persona" or "extra")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (always.Contains(prop.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (prop.Value is JObject shelf && shelf.Properties().Any())
|
||||
{
|
||||
want.Add(prop.Name);
|
||||
}
|
||||
}
|
||||
string extra = parts["extra"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(extra))
|
||||
{
|
||||
// RenderIdentityBlock with onlyShelves skips extra; append manually below if needed.
|
||||
}
|
||||
}
|
||||
if (want.Count == 0)
|
||||
{
|
||||
JObject parts = LoadIdentityParts(id);
|
||||
string extraOnly = parts["extra"]?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(extraOnly))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return $"## Persona lore: {id}\n\n{extraOnly.Trim()}";
|
||||
}
|
||||
string body = RenderIdentityBlock(id, includeAllShelves: false, onlyShelves: want);
|
||||
JObject all = LoadIdentityParts(id);
|
||||
string extraMd = all["extra"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(extraMd) && (shelfNames is null || !shelfNames.Any() || shelfNames.Any(s => string.Equals(s, "extra", StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
body = string.IsNullOrWhiteSpace(body) ? extraMd.Trim() : body + "\n\n" + extraMd.Trim();
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
static IEnumerable<string> PersonaIdsUnder(string root)
|
||||
{
|
||||
string dir = Path.Combine(root ?? "", "personas");
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SwarmUI.Utils;
|
||||
|
||||
namespace Mrleo1nid.SwarmAssistent;
|
||||
|
||||
/// <summary>Facts about the human at the desk — global + per-persona, separate from craft RAG.</summary>
|
||||
public sealed partial class AssistentMemory
|
||||
{
|
||||
const string MetaTasteMigrated = "user_prefs_taste_migrated";
|
||||
|
||||
void EnsureUserPrefsSchema()
|
||||
{
|
||||
Exec(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_prefs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
scope TEXT NOT NULL DEFAULT 'global',
|
||||
persona_id TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT 'user',
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
updated INTEGER NOT NULL,
|
||||
UNIQUE(key, scope, persona_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_prefs_scope ON user_prefs(scope, persona_id);
|
||||
""");
|
||||
MigrateTasteToUserPrefsOnce();
|
||||
}
|
||||
|
||||
void MigrateTasteToUserPrefsOnce()
|
||||
{
|
||||
if (GetMeta(MetaTasteMigrated) == "1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
JObject taste = GetKvObject(KvTaste);
|
||||
if (taste is not null && taste.Count > 0)
|
||||
{
|
||||
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
void AddList(string prefix, JToken arr)
|
||||
{
|
||||
if (arr is not JArray a)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
foreach (JToken t in a)
|
||||
{
|
||||
string text = (t?.ToString() ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UpsertUserPrefUnlocked($"{prefix}_{i++}", text, "global", "", "migrated_taste", pinned: false, now);
|
||||
}
|
||||
}
|
||||
AddList("like", taste["likes"]);
|
||||
AddList("avoid", taste["avoid"]);
|
||||
AddList("style", taste["styles"]);
|
||||
string notes = (taste["notes"]?.ToString() ?? "").Trim();
|
||||
if (!string.IsNullOrWhiteSpace(notes))
|
||||
{
|
||||
UpsertUserPrefUnlocked("notes", notes, "global", "", "migrated_taste", pinned: false, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory taste→user_prefs: {ex.Message}");
|
||||
}
|
||||
SetMeta(MetaTasteMigrated, "1");
|
||||
}
|
||||
|
||||
static string NormalizePrefScope(string scope)
|
||||
{
|
||||
string s = (scope ?? "").Trim().ToLowerInvariant();
|
||||
return s is "persona" or "personal" or "agent" ? "persona" : "global";
|
||||
}
|
||||
|
||||
static string NormalizePrefPersona(string scope, string personaId)
|
||||
{
|
||||
if (NormalizePrefScope(scope) == "global")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return AssistentConfig.SafeId(personaId) ?? "neutral";
|
||||
}
|
||||
|
||||
public JArray ListUserPrefs(string scope = null, string personaId = null, int limit = 200)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
List<JObject> rows = [];
|
||||
string wantScope = (scope ?? "").Trim().ToLowerInvariant();
|
||||
string wantPersona = AssistentConfig.SafeId(personaId) ?? "";
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
List<string> where = [];
|
||||
if (wantScope is "global" or "shared" or "common")
|
||||
{
|
||||
where.Add("scope = 'global'");
|
||||
}
|
||||
else if (wantScope is "persona" or "personal")
|
||||
{
|
||||
where.Add("scope = 'persona'");
|
||||
if (!string.IsNullOrWhiteSpace(wantPersona))
|
||||
{
|
||||
where.Add("persona_id = $persona");
|
||||
cmd.Parameters.AddWithValue("$persona", wantPersona);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(wantPersona))
|
||||
{
|
||||
// Prompt path: global ∪ this persona
|
||||
where.Add("(scope = 'global' OR (scope = 'persona' AND persona_id = $persona))");
|
||||
cmd.Parameters.AddWithValue("$persona", wantPersona);
|
||||
}
|
||||
string whereSql = where.Count > 0 ? "WHERE " + string.Join(" AND ", where) : "";
|
||||
cmd.CommandText =
|
||||
$"""
|
||||
SELECT id, key, text, scope, persona_id, source, pinned, updated
|
||||
FROM user_prefs
|
||||
{whereSql}
|
||||
ORDER BY pinned DESC, updated DESC
|
||||
LIMIT $lim
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("$lim", Math.Clamp(limit, 1, 2000));
|
||||
using SqliteDataReader reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
string sc = reader.GetString(3);
|
||||
string pid = reader.IsDBNull(4) ? "" : reader.GetString(4) ?? "";
|
||||
rows.Add(new JObject
|
||||
{
|
||||
["id"] = reader.GetInt64(0),
|
||||
["key"] = reader.GetString(1),
|
||||
["text"] = reader.GetString(2),
|
||||
["scope"] = sc,
|
||||
["persona"] = string.IsNullOrEmpty(pid) ? "" : pid,
|
||||
["persona_id"] = pid,
|
||||
["source"] = reader.GetString(5),
|
||||
["pinned"] = !reader.IsDBNull(6) && reader.GetInt64(6) != 0,
|
||||
["updated"] = reader.IsDBNull(7) ? 0 : reader.GetInt64(7),
|
||||
});
|
||||
}
|
||||
return new JArray(rows);
|
||||
}
|
||||
}
|
||||
|
||||
public JObject UpsertUserPref(string key, string text, string scope = "global", string personaId = null, string source = "user", bool? pinned = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
UpsertUserPrefUnlocked(key, text, scope, personaId, source, pinned ?? false, now);
|
||||
return new JObject
|
||||
{
|
||||
["key"] = (key ?? "").Trim(),
|
||||
["text"] = (text ?? "").Trim(),
|
||||
["scope"] = NormalizePrefScope(scope),
|
||||
["persona_id"] = NormalizePrefPersona(scope, personaId),
|
||||
["source"] = string.IsNullOrWhiteSpace(source) ? "user" : source.Trim().ToLowerInvariant(),
|
||||
["pinned"] = pinned ?? false,
|
||||
["updated"] = now,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void UpsertUserPrefUnlocked(string key, string text, string scope, string personaId, string source, bool pinned, long updated)
|
||||
{
|
||||
key = (key ?? "").Trim();
|
||||
text = (text ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
throw new ArgumentException("key and text required");
|
||||
}
|
||||
if (key.Length > 120)
|
||||
{
|
||||
key = key[..120];
|
||||
}
|
||||
string sc = NormalizePrefScope(scope);
|
||||
string pid = NormalizePrefPersona(sc, personaId);
|
||||
string src = string.IsNullOrWhiteSpace(source) ? "user" : source.Trim().ToLowerInvariant();
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"""
|
||||
INSERT INTO user_prefs(key, text, scope, persona_id, source, pinned, updated)
|
||||
VALUES ($key, $text, $scope, $persona, $source, $pinned, $upd)
|
||||
ON CONFLICT(key, scope, persona_id) DO UPDATE SET
|
||||
text = excluded.text,
|
||||
source = excluded.source,
|
||||
pinned = excluded.pinned,
|
||||
updated = excluded.updated
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("$key", key);
|
||||
cmd.Parameters.AddWithValue("$text", text);
|
||||
cmd.Parameters.AddWithValue("$scope", sc);
|
||||
cmd.Parameters.AddWithValue("$persona", pid);
|
||||
cmd.Parameters.AddWithValue("$source", src);
|
||||
cmd.Parameters.AddWithValue("$pinned", pinned ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("$upd", updated);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public bool ForgetUserPref(string key, string scope = "global", string personaId = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
string sc = NormalizePrefScope(scope);
|
||||
string pid = NormalizePrefPersona(sc, personaId);
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM user_prefs WHERE key = $key AND scope = $scope AND persona_id = $persona";
|
||||
cmd.Parameters.AddWithValue("$key", (key ?? "").Trim());
|
||||
cmd.Parameters.AddWithValue("$scope", sc);
|
||||
cmd.Parameters.AddWithValue("$persona", pid);
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int ClearUserPrefs(string scope = null, string personaId = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
string want = (scope ?? "all").Trim().ToLowerInvariant();
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
if (want is "global" or "shared")
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM user_prefs WHERE scope = 'global'";
|
||||
}
|
||||
else if (want is "persona" or "personal")
|
||||
{
|
||||
string pid = AssistentConfig.SafeId(personaId) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(pid))
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM user_prefs WHERE scope = 'persona'";
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM user_prefs WHERE scope = 'persona' AND persona_id = $persona";
|
||||
cmd.Parameters.AddWithValue("$persona", pid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd.CommandText = "DELETE FROM user_prefs";
|
||||
}
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
public int CountUserPrefs()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT COUNT(*) FROM user_prefs";
|
||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Select prefs for prompt injection according to weight (0=off, <1=short, 1=full, >1=must).</summary>
|
||||
public List<JObject> SelectUserPrefsForPrompt(string personaId, double weight, int maxRows)
|
||||
{
|
||||
if (weight <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
string pid = AssistentConfig.SafeId(personaId) ?? "neutral";
|
||||
JArray all = ListUserPrefs(null, pid, 500);
|
||||
List<JObject> list = all.OfType<JObject>().ToList();
|
||||
maxRows = Math.Clamp(maxRows, 1, 40);
|
||||
int take = weight >= 1
|
||||
? maxRows
|
||||
: Math.Max(3, (int)Math.Ceiling(maxRows * weight));
|
||||
return list.Take(take).ToList();
|
||||
}
|
||||
|
||||
public string FormatUserPrefsBlock(string personaId, double weight, int maxRows)
|
||||
{
|
||||
List<JObject> rows = SelectUserPrefsForPrompt(personaId, weight, maxRows);
|
||||
if (rows.Count == 0 || weight <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("## About the user");
|
||||
if (weight > 1.05)
|
||||
{
|
||||
sb.AppendLine("MUST respect these preferences unless the current user message overrides them this turn.");
|
||||
}
|
||||
sb.AppendLine("Facts about the human (global = every persona; persona = this agent only):");
|
||||
foreach (JObject row in rows)
|
||||
{
|
||||
string sc = row["scope"]?.ToString() ?? "global";
|
||||
string key = row["key"]?.ToString() ?? "";
|
||||
string text = row["text"]?.ToString() ?? "";
|
||||
string pin = row["pinned"]?.Value<bool>() == true ? " ★" : "";
|
||||
sb.AppendLine($"- [{sc}] {key}{pin}: {text}");
|
||||
}
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>Batch-delete craft vector rows (never bundled). Returns deleted count.</summary>
|
||||
public int ClearCraftMemory(string scope = null, string kind = null, string persona = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureOpen();
|
||||
using SqliteCommand cmd = _conn.CreateCommand();
|
||||
List<string> where = ["source != 'bundled'"];
|
||||
string wantScope = (scope ?? "").Trim().ToLowerInvariant();
|
||||
if (wantScope is "shared" or "common" or "global")
|
||||
{
|
||||
where.Add("persona = ''");
|
||||
}
|
||||
else if (wantScope is "personal" || !string.IsNullOrWhiteSpace(persona))
|
||||
{
|
||||
string pid = NormalizePersona(persona);
|
||||
where.Add("persona = $persona");
|
||||
cmd.Parameters.AddWithValue("$persona", pid);
|
||||
}
|
||||
string k = (kind ?? "").Trim().ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(k) && k != "all")
|
||||
{
|
||||
where.Add("kind = $kind");
|
||||
cmd.Parameters.AddWithValue("$kind", k);
|
||||
}
|
||||
cmd.CommandText = "DELETE FROM memories WHERE " + string.Join(" AND ", where);
|
||||
return cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,14 @@ public sealed partial class AssistentMemory : IDisposable
|
||||
{
|
||||
Logs.Debug($"AssistentMemory store schema: {ex.Message}");
|
||||
}
|
||||
try
|
||||
{
|
||||
EnsureUserPrefsSchema();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentMemory user_prefs schema: {ex.Message}");
|
||||
}
|
||||
_embedModel = GetMeta("embed_model") ?? _embedModel;
|
||||
_ = int.TryParse(GetMeta("dims"), out _dims);
|
||||
_ = int.TryParse(GetMeta("seed_version"), out _seedVersion);
|
||||
|
||||
@@ -242,6 +242,39 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentClearMemory(Session session, string scope = null, string kind = null, string persona = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (Memory is null)
|
||||
{
|
||||
return new JObject { ["error"] = "memory not ready" };
|
||||
}
|
||||
try
|
||||
{
|
||||
string targetPersona = persona;
|
||||
string wantScope = (scope ?? "").Trim().ToLowerInvariant();
|
||||
if (wantScope is "personal" && string.IsNullOrWhiteSpace(targetPersona))
|
||||
{
|
||||
targetPersona = Config?.DefaultPersonaId() ?? "neutral";
|
||||
}
|
||||
int n = Memory.ClearCraftMemory(scope, kind, targetPersona);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["deleted"] = n,
|
||||
["scope"] = scope ?? "all",
|
||||
["kind"] = kind ?? "all",
|
||||
["persona"] = AssistentMemory.IsShared(AssistentMemory.NormalizePersona(targetPersona))
|
||||
? "shared"
|
||||
: AssistentMemory.NormalizePersona(targetPersona),
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"memory clear: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The gpu-rent wanted queue (models pending the next <c>up</c>) — count + entries.</summary>
|
||||
public async Task<JObject> AssistentListWanted(Session session)
|
||||
{
|
||||
|
||||
+14
-1
@@ -19,7 +19,8 @@ public partial class SwarmAssistentExtension
|
||||
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
|
||||
"creativity", "intensity", "complexity", "movement",
|
||||
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
|
||||
"memory_query", "memory_kind", "tag_query",
|
||||
"memory_query", "memory_kind", "tag_query", "user_prefs",
|
||||
"inventory_query", "skills", "persona_shelves", "controls",
|
||||
];
|
||||
|
||||
static bool HasValue(JObject obj, string key)
|
||||
@@ -146,6 +147,18 @@ public partial class SwarmAssistentExtension
|
||||
{
|
||||
return "lookup_tags";
|
||||
}
|
||||
if (ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString()))
|
||||
{
|
||||
return "list_inventory";
|
||||
}
|
||||
if (ActionsContain(patch, "skill_load"))
|
||||
{
|
||||
return "skill_load";
|
||||
}
|
||||
if (ActionsContain(patch, "persona_read"))
|
||||
{
|
||||
return "persona_read";
|
||||
}
|
||||
if (ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
|
||||
{
|
||||
return "civitai";
|
||||
|
||||
@@ -139,4 +139,120 @@ public partial class SwarmAssistentExtension
|
||||
return new JObject { ["error"] = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentExportPersona(Session session, string persona = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId();
|
||||
try
|
||||
{
|
||||
JObject pack = Config.ExportPersonaPack(pid);
|
||||
return new JObject { ["success"] = true, ["pack"] = pack };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"export persona: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentImportPersona(Session session, JObject pack = null, string new_id = null, bool overwrite = false)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
JObject meta = Config.ImportPersonaPack(pack, new_id, overwrite);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["persona"] = meta,
|
||||
["personas"] = new JArray(Config.ListPersonaCatalog().Select(p => new JObject
|
||||
{
|
||||
["id"] = p.id,
|
||||
["title"] = p.title,
|
||||
["accent"] = p.accent,
|
||||
["source"] = p.source,
|
||||
})),
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentSaveKnobs(Session session, JObject assistant = null, JObject exact = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
JObject asstOut = null;
|
||||
JObject exactOut = null;
|
||||
if (assistant is not null && assistant.Count > 0)
|
||||
{
|
||||
JObject sparse = new();
|
||||
foreach (string key in new[] { "num_ctx", "history_keep_turns", "memory_top_k", "user_prefs_weight", "user_prefs_max" })
|
||||
{
|
||||
if (assistant[key] is not null)
|
||||
{
|
||||
sparse[key] = assistant[key];
|
||||
}
|
||||
}
|
||||
if (sparse.Count > 0)
|
||||
{
|
||||
asstOut = Config.MergeOverlayBaseJson("assistant.json", sparse);
|
||||
}
|
||||
}
|
||||
if (exact is not null && exact.Count > 0)
|
||||
{
|
||||
JObject sparse = new();
|
||||
if (exact["profiles"] is JObject profiles)
|
||||
{
|
||||
JObject slimProfiles = new();
|
||||
foreach (string name in new[] { "turbo", "raw" })
|
||||
{
|
||||
if (profiles[name] is JObject p)
|
||||
{
|
||||
JObject slim = new();
|
||||
foreach (string k in new[] { "steps", "cfg", "sigma_shift" })
|
||||
{
|
||||
if (p[k] is not null)
|
||||
{
|
||||
slim[k] = p[k];
|
||||
}
|
||||
}
|
||||
if (slim.Count > 0)
|
||||
{
|
||||
slimProfiles[name] = slim;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slimProfiles.Count > 0)
|
||||
{
|
||||
sparse["profiles"] = slimProfiles;
|
||||
}
|
||||
}
|
||||
if (exact["generation"] is JObject gen)
|
||||
{
|
||||
sparse["generation"] = gen;
|
||||
}
|
||||
if (sparse.Count > 0)
|
||||
{
|
||||
exactOut = Config.MergeOverlayBaseJson("exact.json", sparse);
|
||||
}
|
||||
}
|
||||
string pid = Config.DefaultPersonaId();
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["assistant"] = Config.LoadAssistant(pid),
|
||||
["exact"] = Config.LoadExact(pid),
|
||||
["overlay_assistant"] = asstOut,
|
||||
["overlay_exact"] = exactOut,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"save knobs: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SwarmUI.Accounts;
|
||||
|
||||
namespace Mrleo1nid.SwarmAssistent;
|
||||
|
||||
/// <summary>CRUD for About-the-user preferences (global + per-persona).</summary>
|
||||
public partial class SwarmAssistentExtension
|
||||
{
|
||||
public async Task<JObject> AssistentListUserPrefs(Session session, string scope = null, string persona = null, int limit = 200)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (Memory is null)
|
||||
{
|
||||
return new JObject { ["error"] = "memory not ready" };
|
||||
}
|
||||
try
|
||||
{
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config?.DefaultPersonaId() ?? "neutral";
|
||||
JArray rows = Memory.ListUserPrefs(scope, pid, limit);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["prefs"] = rows,
|
||||
["total"] = Memory.CountUserPrefs(),
|
||||
["persona"] = pid,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"user prefs list: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentUpsertUserPref(Session session, string key, string text, string scope = "global", string persona = null, string source = "user", bool pinned = false)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (Memory is null)
|
||||
{
|
||||
return new JObject { ["error"] = "memory not ready" };
|
||||
}
|
||||
try
|
||||
{
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config?.DefaultPersonaId() ?? "neutral";
|
||||
JObject row = Memory.UpsertUserPref(key, text, scope, pid, source, pinned);
|
||||
return new JObject { ["success"] = true, ["pref"] = row };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"user pref upsert: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentForgetUserPref(Session session, string key, string scope = "global", string persona = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (Memory is null)
|
||||
{
|
||||
return new JObject { ["error"] = "memory not ready" };
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return new JObject { ["error"] = "key required" };
|
||||
}
|
||||
try
|
||||
{
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config?.DefaultPersonaId() ?? "neutral";
|
||||
bool ok = Memory.ForgetUserPref(key, scope, pid);
|
||||
return new JObject { ["success"] = ok, ["key"] = key.Trim(), ["scope"] = scope };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"user pref forget: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentClearUserPrefs(Session session, string scope = "all", string persona = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (Memory is null)
|
||||
{
|
||||
return new JObject { ["error"] = "memory not ready" };
|
||||
}
|
||||
try
|
||||
{
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config?.DefaultPersonaId() ?? "neutral";
|
||||
int n = Memory.ClearUserPrefs(scope, pid);
|
||||
return new JObject { ["success"] = true, ["deleted"] = n, ["scope"] = scope ?? "all" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new JObject { ["error"] = $"user prefs clear: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,21 @@
|
||||
"max_checkpoints_inventory": 60,
|
||||
"max_wildcards_inventory": 80,
|
||||
"inventory_blurb_max": 140,
|
||||
"inventory_prompt_rich": 12,
|
||||
"inventory_prompt_names": 24,
|
||||
"inventory_hop_limit": 20,
|
||||
"max_ref_slots": 4,
|
||||
"default_pack": "write_prompt",
|
||||
"default_persona": "neutral",
|
||||
"embed_model": "nomic-embed-text",
|
||||
"memory_top_k": 10,
|
||||
"memory_top_k": 8,
|
||||
"memory_hit_chars": 240,
|
||||
"memory_min_score": 0.32,
|
||||
"user_prefs_weight": 1.0,
|
||||
"user_prefs_max": 16,
|
||||
"max_tool_hops": 4,
|
||||
"tag_lookup_limit": 20,
|
||||
"identity_always_shelves": ["persona", "voice", "rules", "likes", "dislikes"],
|
||||
"memory_quotas": {
|
||||
"card": 3,
|
||||
"lora": 3,
|
||||
|
||||
+40
-82
@@ -6,110 +6,68 @@ You are **Swarm Assistent**, a collaborative art director for image generation i
|
||||
|
||||
When instructions conflict, apply this order (highest wins):
|
||||
|
||||
1. **This core contract** — output format, never invent LoRA/checkpoint names, never use CFG 0, never depict or request anyone 17 or under (adults only).
|
||||
1. **This core contract** — output format, never invent LoRA/checkpoint names or triggers, never use CFG 0, never depict or request anyone 17 or under (adults only).
|
||||
2. **Current user message** — explicit “use steps 20 / aspect 16:9 now” wins for that turn.
|
||||
3. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat).
|
||||
4. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged into it.
|
||||
5. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change.
|
||||
6. **`memory_hits` (hybrid FTS + vector RAG)** — notes, pitfalls, LoRA blurbs. Shared hits apply to every persona; personal hits overwrite shared on the same kind+key. Never override exact numbers or the user’s param request. For a missing exact row use `memory_get`; for a second search use `memory_search`; for Danbooru spelling/aliases use `lookup_tags` (do not dump tag soup into Krea prompts).
|
||||
7. Guesses — last resort only.
|
||||
3. **About the user** (`## About the user`) — durable preferences (global + this persona). Respect unless this turn overrides.
|
||||
4. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat).
|
||||
5. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged.
|
||||
6. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change.
|
||||
7. **`memory_hits` (hybrid FTS + vector RAG)** — craft notes / LoRA blurbs (often truncated). Prefer over guesses; never override Exact, About the user, or the user’s param request. Full row → `memory_get`; more search → `memory_search`; Danbooru spelling → `lookup_tags` (no tag soup in Krea prompts).
|
||||
8. Guesses — last resort only.
|
||||
|
||||
Exact = encyclopedia of defaults. RAG = soft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match exact (or session_exact) and the user did not ask to change them.
|
||||
Exact = defaults encyclopedia. About the user = human taste. RAG = soft craft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact (or `session_exact`) and the user did not ask to change them.
|
||||
|
||||
Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. If you propose UI changes, the fence is mandatory. Keep the prose short (a few lines) — do not paste long critique templates.
|
||||
Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. Keep prose short (a few lines).
|
||||
|
||||
## Live context
|
||||
|
||||
A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth — refreshed every chat turn:
|
||||
"Live SwarmUI context" JSON is ground truth for this turn:
|
||||
|
||||
- Use only LoRAs listed in `available_loras` / `enabled_loras` (exact `name`), or Civitai search candidates.
|
||||
- Prefer listed `trigger_phrase` / `triggers` — **never invent** trigger words.
|
||||
- `session_exact` / `recommended_params` — session overrides and defaults (Exact KV is in the system block above).
|
||||
- `memory_hits` are retrieved notes (hybrid FTS+vector). Each hit has `scope` (`shared`|`personal`). Trust them over guesses, but **not** over Exact or the user.
|
||||
- `has_vision_image` — if false, do not invent what the image looks like; emit `look_at` first when you need to see it.
|
||||
- `model_cards` for **enabled** models beat generic blurbs — follow `when` / `avoid` / `prompt_hint` / `triggers`.
|
||||
- `taste_profile` is the user's remembered preferences — bias toward it unless they override.
|
||||
- Prefer `krea_likely` / Krea architecture entries; ignore FLUX/SDXL LoRAs.
|
||||
- Respect current width/height/steps/cfg/seed/sigma_shift/sampler unless the user asks or the pack is `fix_params`.
|
||||
- `wildcards` — `__name__` syntax. `prompt_image_count` > 0 means Prompt Images may dominate text.
|
||||
- **Init / inpaint:** `has_init_image`, `has_mask_image`, `init_creativity` (denoise 0–1), `mask_blur`, `mask_grow`.
|
||||
- **Board:** `image_slots`. `generate` = live gen. `ref1`… = refs. Emit `look_at` to see an unattached window.
|
||||
- Use only LoRA/checkpoint **names** from `available_loras` / `enabled_loras` (or Civitai hop results). Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**.
|
||||
- Rich entries (blurbs/triggers) are enabled + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
|
||||
- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
|
||||
- `has_vision_image` true means a real board frame exists, but **images are not in this request** until you emit `look_at`. Do not invent what the image looks like.
|
||||
- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`.
|
||||
- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields (aspect, inpaint, persona authoring) are documented in the active pack.
|
||||
|
||||
## Memory (short)
|
||||
|
||||
- Craft RAG write: `memory_upsert` / `memory_forget` + `memories: [{kind,key,text,scope}]` (default personal).
|
||||
- About the user: `user_pref_upsert` / `user_pref_forget` + `user_prefs: [{key,text,scope}]`. Do **not** put human taste into craft `memories`.
|
||||
- Fat memory skill text: `skill_load` + `skills: ["memory"]` when you need the full write/read playbook.
|
||||
|
||||
## Output contract (mandatory)
|
||||
|
||||
1. Write a short helpful reply in the user's language (RU or EN).
|
||||
2. Then emit **one** fenced JSON patch (only fields you want to change):
|
||||
1. Short helpful reply in the user's language (RU or EN).
|
||||
2. One fenced JSON patch with **only fields you want to change**:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "...",
|
||||
"negative": null,
|
||||
"loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["..."]}],
|
||||
"prompt": "A fluffy red fox in fresh powder snow, soft morning light, 85mm f/2.8…",
|
||||
"loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["listed_trigger"]}],
|
||||
"aspect": "16:9",
|
||||
"width": 1376,
|
||||
"height": 768,
|
||||
"steps": 8,
|
||||
"cfg": 1,
|
||||
"seed": -1,
|
||||
"images": 1,
|
||||
"sigma_shift": 1.15,
|
||||
"sampler": null,
|
||||
"creativity": "medium",
|
||||
"intensity": 0,
|
||||
"complexity": 0,
|
||||
"movement": 0,
|
||||
"vary": false,
|
||||
"lock_seed": false,
|
||||
"use_init_image": false,
|
||||
"clear_init_image": false,
|
||||
"init_creativity": 0.45,
|
||||
"use_mask_image": false,
|
||||
"clear_mask_image": false,
|
||||
"mask_blur": null,
|
||||
"mask_grow": null,
|
||||
"clear_prompt_images": false,
|
||||
"slot_to_prompt_image": null,
|
||||
"look_at": ["generate"],
|
||||
"slot_to_init": null,
|
||||
"slot_to_mask": null,
|
||||
"snapshot_generate": false,
|
||||
"select_slot": null,
|
||||
"pack": null,
|
||||
"actions": ["generate"],
|
||||
"search_query": null,
|
||||
"memory_query": null,
|
||||
"memory_kind": null,
|
||||
"tag_query": null,
|
||||
"memories": [{"kind": "lora", "key": "name", "text": "fact", "scope": "personal"}],
|
||||
"controls": {"preference_bias": 0.35},
|
||||
"persona_clone": null,
|
||||
"persona_shelves": null,
|
||||
"persona": null,
|
||||
"notes": "one-line why"
|
||||
}
|
||||
```
|
||||
|
||||
### Patch rules
|
||||
|
||||
- Omit keys you are not changing.
|
||||
- Prefer omitting `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact memory (or `session_exact`) and the user did not request a change — the UI fills empties from exact.
|
||||
- `loras` replaces the intended LoRA set for Apply (list all that should be on).
|
||||
- Prefer `aspect` over raw width/height when framing changes.
|
||||
- `vary: true` — new random seed. `lock_seed: true` — reuse current seed.
|
||||
- `pack` — switch active prompt pack for a follow-up hop.
|
||||
- `controls` — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Do not invent control ids.
|
||||
- `persona_clone` / `persona_shelves` / `actions` with `persona_clone`|`persona_write`|`persona_switch` — only in `author_persona` pack. Overlay-only; never delete personas from a patch.
|
||||
- Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`.
|
||||
- `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height.
|
||||
- Optional keys (seed, vary, init/mask, creativity/sliders, pack, controls, persona authoring, search/memory queries) — use when needed; packs list the ones for that mode.
|
||||
- Do not invent model or LoRA filenames.
|
||||
- Memory: `memory_upsert` / `memory_forget` with `memories: [{kind,key,text,scope}]`. Default scope is personal. Tools: `memory_get` + kind/key, `memory_search` + `memory_query`, `lookup_tags` + `tag_query` (Danbooru csv — spelling only, not prompt soup).
|
||||
|
||||
### Actions (auto-safe)
|
||||
### Actions / hops
|
||||
|
||||
- `"generate"` — after Apply, start generation. When the user explicitly asks to generate, always include this; the UI applies silently (no Apply-button strip).
|
||||
- `"search_civitai"` — Civitai search; user Confirms downloads.
|
||||
- `"generate"` — Apply + start generation when the user wants a new image.
|
||||
- `"search_civitai"` + `search_query` — Civitai hop (user Confirms downloads).
|
||||
- `"interrupt"` — stop generation.
|
||||
- `"memory_upsert"` / `"memory_forget"` — write or delete vector memory (personal by default; `scope: "shared"` for the common store).
|
||||
- `"memory_get"` / `"memory_search"` — hop: exact row or hybrid search.
|
||||
- `"lookup_tags"` — hop: Danbooru csv (aliases/counts). Do not emit tag soup for Krea.
|
||||
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — author_persona only. Never `"persona_delete"`.
|
||||
- `look_at: ["generate", "ref1"]` — vision hop.
|
||||
- Pure Q&A with no change: omit the JSON patch.
|
||||
- `"memory_get"` / `"memory_search"` / `"lookup_tags"` — read hops.
|
||||
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
|
||||
- `"skill_load"` + `skills: ["memory"]` — load fat skill text.
|
||||
- `"persona_read"` — load lore shelves (appearance/outfits/…) not in always-on identity.
|
||||
- `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes.
|
||||
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`.
|
||||
- `look_at: ["generate"|"ref1"|…]` — vision hop (JPEG arrives on the follow-up).
|
||||
- Pure Q&A: omit the JSON patch.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "memory",
|
||||
"title": "Exact + vector memory",
|
||||
"default": true,
|
||||
"default": false,
|
||||
"prompt_file": "memory.md"
|
||||
}
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
# Skill: memory
|
||||
|
||||
You have three memory tools:
|
||||
You have four memory tools:
|
||||
|
||||
1. **Exact memory** (`## Exact memory` + live `exact` / `session_exact`) — canonical KV defaults. Always prefer Exact over RAG for numbers.
|
||||
2. **Vector memory** (`memory_hits`) — hybrid FTS+cosine notes (LoRA tips, pitfalls, paths, cards). Shared + this persona; personal overwrites shared on the same `kind`+`key`.
|
||||
3. **Tag catalog** (`lookup_tags`) — Danbooru csv (canonical name, aliases, post_count). **Not** RAG. Krea prompts stay natural prose; use this to check spelling/aliases only.
|
||||
2. **About the user** (`## About the user`) — durable human preferences (global across personas + personal for this agent). Tunable weight in settings. Prefer this for taste (“no blondes”, preferred aspect, NSFW ok for this persona).
|
||||
3. **Vector craft memory** (`memory_hits`) — hybrid FTS+cosine notes (LoRA tips, pitfalls, paths, cards). Shared + this persona; personal overwrites shared on the same `kind`+`key`.
|
||||
4. **Tag catalog** (`lookup_tags`) — Danbooru csv (canonical name, aliases, post_count). **Not** RAG. Krea prompts stay natural prose; use this to check spelling/aliases only.
|
||||
|
||||
## Priority
|
||||
|
||||
User (this turn) > `session_exact` > Exact KV > filled live fields > `memory_hits` > guesses. Never let a vector hit override Exact steps/CFG/aspect or an explicit user param request.
|
||||
User (this turn) > About the user > `session_exact` > Exact KV > filled live fields > craft `memory_hits` > guesses. Never let a vector hit override Exact steps/CFG/aspect, About the user, or an explicit user param request.
|
||||
|
||||
## Read tools (hop, like Civitai)
|
||||
|
||||
- `memory_get` + `memories: [{kind,key}]` — exact row (personal overlay if any).
|
||||
- `memory_get` + `memories: [{kind,key}]` — exact craft row (personal overlay if any).
|
||||
- `memory_search` + `memory_query` (optional `memory_kind`) — hybrid search when `memory_hits` are not enough.
|
||||
- `lookup_tags` + `tag_query` — csv lookup. Do **not** paste tag soup into the prompt.
|
||||
|
||||
Omit the tool action on the follow-up turn once you have results.
|
||||
|
||||
## When to write (vector only)
|
||||
## When to write — About the user
|
||||
|
||||
- Durable facts about the human: likes/avoids, hard constraints, working style with this agent.
|
||||
- Prefer `actions: ["user_pref_upsert"]` + `user_prefs: [{ "key": "stable-id", "text": "…", "scope": "global"|"persona" }]`.
|
||||
- Default **`scope: "global"`** for preferences every persona should honor. Use `"persona"` only when it is specific to this agent’s dynamic.
|
||||
- Forget with `user_pref_forget` + same key/scope (omit text).
|
||||
|
||||
## When to write — craft vector only
|
||||
|
||||
- Durable facts about a LoRA/checkpoint (when it works, what it breaks, good weight).
|
||||
- Bad paths / pitfalls you discovered this session.
|
||||
@@ -28,7 +36,7 @@ Omit the tool action on the follow-up turn once you have results.
|
||||
## When not to write
|
||||
|
||||
- Do not dump Exact defaults or the full inventory into vector memory.
|
||||
- Do not store the user's taste profile (sqlite `kv.taste`).
|
||||
- Do not upsert trivia already in `memory_hits`.
|
||||
- Do not store the user's taste in craft `memories` — use `user_prefs` instead.
|
||||
- Do not upsert trivia already in `memory_hits` or already listed under About the user.
|
||||
- Do not upsert Danbooru tags — the csv catalog already has them.
|
||||
- `memory_forget` without `scope` only removes the personal overlay.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Swarm Assistent
|
||||
|
||||
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **vector memory**, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
|
||||
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
|
||||
|
||||
**Version 0.9.0** — Persona **shelves** (short JSON files, nested identity markdown), per-persona **Exact controls** (e.g. Leonid `preference_bias` slider), overlay **clone/author** pack (`/persona new`), UI-only delete for overlay personas. Chats/UI/taste stay in `assistent.sqlite`.
|
||||
**Version 0.10.0** — Settings panel (Поведение / Модели / Личности / О пользователе / Крафт / Ещё), **UserPrefs** (global + per-persona) with prompt weight, persona export/import, craft memory clear APIs. Legacy `taste` migrates into UserPrefs once.
|
||||
|
||||
## Layout
|
||||
|
||||
- **Left — Board tabs:** **Generate** (full-height live view) | **Refs** (reference grid + badge `N · vision M`); **Посмотри результат** attaches the finished frame and asks for a verdict
|
||||
- **Splitter:** drag to resize panes
|
||||
- **Right:** Chat | Cards; persona / pack / Ollama chat model; **Ollama health** badge; settings gear (memory model + skills + **Память**)
|
||||
- **Right:** Chat | Cards; persona / pack / Ollama chat model; **Ollama health** badge; ⚙ settings panel (6 tabs)
|
||||
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
|
||||
|
||||
## Config (bundled + overlay)
|
||||
@@ -26,7 +26,7 @@ Assistent/
|
||||
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
||||
settings.json # embed_model, base_url, per-persona skills
|
||||
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
|
||||
memory/assistent.sqlite # vector memory + tags FTS + chats + ui_state + taste
|
||||
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state + taste (legacy)
|
||||
_migrated_json/ # one-shot archive of old chats/*.json, ui-state.json, taste.json
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/person
|
||||
|
||||
**Controls:** optional `controls.json` schema + `exact.controls` values. UI shows sliders; LLM may patch `"controls": {…}`. Values persist in overlay Exact (not session_exact).
|
||||
|
||||
**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button (never from the model).
|
||||
**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model). Export/import `.assistent-persona.json` for sharing.
|
||||
|
||||
## Exact memory (KV)
|
||||
|
||||
@@ -42,19 +42,28 @@ Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/person
|
||||
- Persona / disk overlays merge via DeepMerge (matching keys overwrite)
|
||||
- Always injected into the system prompt; UI fills **empty** SwarmUI fields from Exact (no LLM call)
|
||||
- Chat-session overrides (`session_exact`) last until persona change or clear chat — not written to disk
|
||||
- Priority: core contract → current user → session_exact → exact (+ persona) → live fields → vector `memory_hits`
|
||||
- Priority: core → user message → **About the user** → session_exact → exact (+ persona) → live fields → craft `memory_hits`
|
||||
|
||||
## Vector memory
|
||||
## About the user (UserPrefs)
|
||||
|
||||
Separate sqlite table `user_prefs` (not craft RAG):
|
||||
|
||||
- **Global** — every persona (e.g. “avoid blonde hair”)
|
||||
- **Persona** — only the current agent
|
||||
- Injected as `## About the user`; strength via `user_prefs_weight` / `user_prefs_max` in `assistant.json` (⚙ → О пользователе)
|
||||
- Agent write: `actions: ["user_pref_upsert"]` + `user_prefs: [{key,text,scope}]`
|
||||
- Legacy `kv.taste` migrates once into global prefs
|
||||
|
||||
## Craft vector memory
|
||||
|
||||
Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
|
||||
|
||||
- **Shared** — `Config/_base/memory-seed/`, model cards, `scope: "shared"` upserts. Visible to every persona.
|
||||
- **Personal** — `Config/personas/<id>/memory-seed/` and chat upserts (default). Never copied into shared. Other personas do not retrieve it.
|
||||
- Retrieve = shared ∪ this persona (and `extends` parents). Hybrid **FTS5 + cosine**, kind quotas (e.g. 3 cards / 3 pitfalls / 4 notes), `min_score`. Same `kind`+`key`: personal overwrites parent overwrites shared.
|
||||
- Retrieve = shared ∪ this persona (and `extends` parents). Hybrid **FTS5 + cosine**, kind quotas, `min_score`. Same `kind`+`key`: personal overwrites parent overwrites shared.
|
||||
- Tools: `memory_get`, `memory_search`, `lookup_tags` (Danbooru csv in `Data/Autocompletions`, FTS, **no embeddings**).
|
||||
- SQLite + Ollama `/api/embed` (default `nomic-embed-text`, pick in ⚙)
|
||||
- Soft notes only — Exact and the user beat RAG for params
|
||||
- ⚙ → **Память** lists every row (scope · source · date) with a per-row forget; bundled rows are read-only because reseed brings them back
|
||||
- Soft craft notes only — Exact, About the user, and the user beat RAG for params
|
||||
- ⚙ → **Крафт** lists rows with filters + clear (bundled seed is read-only)
|
||||
|
||||
## Chats and runtime KV
|
||||
|
||||
@@ -116,11 +125,11 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
||||
|
||||
## Packs & skills
|
||||
|
||||
**Packs** (one active): `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`.
|
||||
**Packs** (one active): `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`.
|
||||
|
||||
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG.
|
||||
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
|
||||
|
||||
**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`, `leonid` under `Config/personas/`. Overlay clones via `/persona new`.
|
||||
**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`, `leonid` under `Config/personas/`. Overlay clones via `/persona new` or ⚙ → Личности.
|
||||
|
||||
## API routes
|
||||
|
||||
@@ -133,6 +142,8 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
||||
| `AssistentClonePersona` | Snapshot clone → overlay id |
|
||||
| `AssistentSavePersona` | Sparse shelf write (overlay only) |
|
||||
| `AssistentDeletePersona` | UI-only delete of overlay persona |
|
||||
| `AssistentExportPersona` / `AssistentImportPersona` | Shareable `.assistent-persona.json` pack |
|
||||
| `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles |
|
||||
| `AssistentGetSettings` / `AssistentSaveSettings` | Overlay settings (skills, embed_model) |
|
||||
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
|
||||
| `AssistentListPersonas` | Persona catalog |
|
||||
@@ -140,10 +151,11 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
||||
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
|
||||
| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
|
||||
| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) |
|
||||
| `AssistentGetTaste` / `AssistentSaveTaste` | sqlite `kv.taste` |
|
||||
| `AssistentGetTaste` / `AssistentSaveTaste` | sqlite `kv.taste` (legacy; prefer UserPrefs) |
|
||||
| `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user |
|
||||
| `AssistentSearchCivitai` | Civitai LoRA search |
|
||||
| `AssistentChat` / `AssistentChatWS` | Chat (+ hybrid memory + Civitai/tag hops) |
|
||||
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` | Vector store (optional `scope` / `persona`) |
|
||||
| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) |
|
||||
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` / `AssistentClearMemory` | Craft vector store |
|
||||
| `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key |
|
||||
| `AssistentLookupTags` | Danbooru csv FTS (no embeddings) |
|
||||
| `AssistentListChats` / `AssistentGetChat` / `AssistentSaveChat` / `AssistentDeleteChat` | sqlite `chats` (optional `q` FTS) |
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class SwarmAssistentExtension : Extension
|
||||
ExtensionAuthor = "mrleo1nid";
|
||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||
License = "MIT";
|
||||
Version = "0.9.0";
|
||||
Version = "0.10.0";
|
||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||
}
|
||||
|
||||
@@ -81,7 +81,15 @@ public partial class SwarmAssistentExtension : Extension
|
||||
API.RegisterAPICall(AssistentClonePersona, true, PermUse);
|
||||
API.RegisterAPICall(AssistentSavePersona, true, PermUse);
|
||||
API.RegisterAPICall(AssistentDeletePersona, true, PermUse);
|
||||
Logs.Init("Swarm Assistent extension loaded (persona shelves + controls + overlay clone)");
|
||||
API.RegisterAPICall(AssistentExportPersona, false, PermUse);
|
||||
API.RegisterAPICall(AssistentImportPersona, true, PermUse);
|
||||
API.RegisterAPICall(AssistentSaveKnobs, true, PermUse);
|
||||
API.RegisterAPICall(AssistentListUserPrefs, false, PermUse);
|
||||
API.RegisterAPICall(AssistentUpsertUserPref, true, PermUse);
|
||||
API.RegisterAPICall(AssistentForgetUserPref, true, PermUse);
|
||||
API.RegisterAPICall(AssistentClearUserPrefs, true, PermUse);
|
||||
API.RegisterAPICall(AssistentClearMemory, true, PermUse);
|
||||
Logs.Init("Swarm Assistent extension loaded (settings panel + user prefs + craft memory)");
|
||||
}
|
||||
|
||||
int CfgInt(string key, int fallback)
|
||||
|
||||
+136
-25
@@ -73,33 +73,144 @@
|
||||
</div>
|
||||
</header>
|
||||
<div class="sa-settings" id="sa_settings" hidden>
|
||||
<label>Ollama URL <input type="text" id="sa_base_url" value="http://127.0.0.1:11434" /></label>
|
||||
<label>Модель памяти
|
||||
<select id="sa_embed_model" class="sa-select" title="Хранение и группировка памяти (use: memory)">
|
||||
<option value="nomic-embed-text">nomic-embed-text</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="basic-button" id="sa_btn_refresh_models">Обновить модели</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_refresh_inventory">Обновить inventory</button>
|
||||
<div class="sa-skills-label">Скилы (процедуры)</div>
|
||||
<div class="sa-skills-box" id="sa_skills_box"></div>
|
||||
<div class="sa-mem-head">
|
||||
<span class="sa-skills-label">Память</span>
|
||||
<select id="sa_mem_kind" class="sa-select sa-mem-kind" title="Фильтр по типу">
|
||||
<option value="all">Все типы</option>
|
||||
</select>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_mem_refresh" title="Перечитать память и очередь wanted">⟳</button>
|
||||
<div class="sa-settings-head">
|
||||
<strong>Настройки</strong>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_settings_close" title="Закрыть" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="sa-mem-list" id="sa_mem_list"></div>
|
||||
<div class="sa-mem-foot">
|
||||
<span class="sa-mem-total" id="sa_mem_total">Всего: —</span>
|
||||
<span class="sa-mem-wanted" id="sa_mem_wanted" title="Модели в очереди на следующий gpu-rent up">Очередь wanted: —</span>
|
||||
<div class="sa-settings-tabs" role="tablist" aria-label="Разделы настроек">
|
||||
<button type="button" class="sa-stab sa-stab-active" data-stab="behavior" role="tab" aria-selected="true">Поведение</button>
|
||||
<button type="button" class="sa-stab" data-stab="models" role="tab" aria-selected="false">Модели</button>
|
||||
<button type="button" class="sa-stab" data-stab="personas" role="tab" aria-selected="false">Личности</button>
|
||||
<button type="button" class="sa-stab" data-stab="user" role="tab" aria-selected="false">О пользователе</button>
|
||||
<button type="button" class="sa-stab" data-stab="craft" role="tab" aria-selected="false">Крафт</button>
|
||||
<button type="button" class="sa-stab" data-stab="more" role="tab" aria-selected="false">Ещё</button>
|
||||
</div>
|
||||
<div class="sa-settings-panes">
|
||||
<div class="sa-spane" data-spane="behavior">
|
||||
<p class="sa-settings-hint">Автодействия после ответа модели и скилы текущей личности.</p>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_vision" /> Авто-прикреплять Generate к чату</label>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Авто-Generate после патча</label>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
|
||||
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
|
||||
<div class="sa-skills-label">Скилы (процедуры)</div>
|
||||
<div class="sa-skills-box" id="sa_skills_box"></div>
|
||||
</div>
|
||||
<div class="sa-spane" data-spane="models" hidden>
|
||||
<p class="sa-settings-hint">Ollama и модель эмбеддингов для крафт-памяти.</p>
|
||||
<label>Ollama URL <input type="text" id="sa_base_url" value="http://127.0.0.1:11434" /></label>
|
||||
<label>Модель чата
|
||||
<select id="sa_settings_chat_model" class="sa-select" title="Синхрон с шапкой">
|
||||
<option value="">—</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Модель памяти (embed)
|
||||
<select id="sa_embed_model" class="sa-select" title="Хранение и группировка памяти (use: memory)">
|
||||
<option value="nomic-embed-text">nomic-embed-text</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="sa-settings-row">
|
||||
<button type="button" class="basic-button" id="sa_btn_refresh_models">Обновить модели</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_refresh_inventory">Обновить inventory</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_settings_health">Проверить Ollama</button>
|
||||
</div>
|
||||
<div class="sa-settings-health" id="sa_settings_health_line">Ollama · …</div>
|
||||
</div>
|
||||
<div class="sa-spane" data-spane="personas" hidden>
|
||||
<p class="sa-settings-hint">Встроенные личности нельзя удалить. Свои — экспорт / импорт / удаление.</p>
|
||||
<div class="sa-persona-panel">
|
||||
<div class="sa-persona-list" id="sa_persona_list"></div>
|
||||
<div class="sa-persona-preview" id="sa_persona_preview">
|
||||
<div class="sa-mem-empty">Выбери личность слева</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-settings-row">
|
||||
<button type="button" class="basic-button" id="sa_btn_persona_export">Экспорт</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_persona_import">Импорт…</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_persona_clone">Клонировать</button>
|
||||
<button type="button" class="basic-button sa-danger-btn" id="sa_btn_persona_delete_panel" disabled title="Только overlay">Удалить</button>
|
||||
<input type="file" id="sa_persona_import_file" accept=".json,.assistent-persona.json,application/json" hidden />
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-spane" data-spane="user" hidden>
|
||||
<p class="sa-settings-hint">Факты о тебе: общие для всех агентов и личные для текущей личности. Вес управляет силой в промпте.</p>
|
||||
<label>Вес в промпте <span id="sa_user_prefs_weight_val">1.0</span>
|
||||
<input type="range" id="sa_user_prefs_weight" min="0" max="1.5" step="0.1" value="1" />
|
||||
</label>
|
||||
<p class="sa-settings-hint">0 = выкл · 0.5 = кратко · 1 = полный список · >1 = жёстко соблюдать</p>
|
||||
<div class="sa-user-prefs-cols">
|
||||
<div>
|
||||
<div class="sa-mem-head">
|
||||
<span class="sa-skills-label">Общие</span>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_pref_add_global" title="Добавить">+</button>
|
||||
</div>
|
||||
<div class="sa-mem-list" id="sa_prefs_global"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sa-mem-head">
|
||||
<span class="sa-skills-label">Для этой личности</span>
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_pref_add_persona" title="Добавить">+</button>
|
||||
</div>
|
||||
<div class="sa-mem-list" id="sa_prefs_persona"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-settings-row">
|
||||
<button type="button" class="basic-button" id="sa_btn_prefs_refresh">Обновить</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_prefs_clear_global">Очистить общие</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_prefs_clear_persona">Очистить личность</button>
|
||||
<button type="button" class="basic-button sa-danger-btn" id="sa_btn_prefs_clear_all">Очистить всё</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-spane" data-spane="craft" hidden>
|
||||
<p class="sa-settings-hint">Крафт-RAG: LoRA, карточки, pitfalls. Не путать с «О пользователе».</p>
|
||||
<div class="sa-mem-head">
|
||||
<span class="sa-skills-label">Крафт-память</span>
|
||||
<select id="sa_mem_scope" class="sa-select sa-mem-kind" title="Слой">
|
||||
<option value="all">Все слои</option>
|
||||
<option value="shared">Общая</option>
|
||||
<option value="personal">Эта личность</option>
|
||||
</select>
|
||||
<select id="sa_mem_kind" class="sa-select sa-mem-kind" title="Фильтр по типу">
|
||||
<option value="all">Все типы</option>
|
||||
</select>
|
||||
<input type="search" id="sa_mem_search" class="sa-mem-search" placeholder="Поиск…" autocomplete="off" />
|
||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_mem_refresh" title="Перечитать память и очередь wanted">⟳</button>
|
||||
</div>
|
||||
<div class="sa-mem-list" id="sa_mem_list"></div>
|
||||
<div class="sa-mem-foot">
|
||||
<span class="sa-mem-total" id="sa_mem_total">Всего: —</span>
|
||||
<span class="sa-mem-wanted" id="sa_mem_wanted" title="Модели в очереди на следующий gpu-rent up">Очередь wanted: —</span>
|
||||
</div>
|
||||
<label>memory_top_k <input type="number" id="sa_memory_top_k" min="1" max="30" step="1" value="10" /></label>
|
||||
<div class="sa-settings-row">
|
||||
<button type="button" class="basic-button" id="sa_btn_mem_clear_kind">Очистить тип</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_mem_clear_persona">Очистить личность</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_mem_clear_shared">Очистить общую</button>
|
||||
<button type="button" class="basic-button sa-danger-btn" id="sa_btn_mem_clear_all">Очистить всё крафт</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-spane" data-spane="more" hidden>
|
||||
<p class="sa-settings-hint">Контекст Ollama и Exact Turbo/RAW (пишется в overlay).</p>
|
||||
<label>num_ctx <input type="number" id="sa_num_ctx" min="2048" max="131072" step="1024" value="16384" /></label>
|
||||
<label>history_keep_turns <input type="number" id="sa_history_keep" min="1" max="32" step="1" value="4" /></label>
|
||||
<div class="sa-skills-label">Exact · Turbo</div>
|
||||
<div class="sa-settings-row sa-knob-row">
|
||||
<label>steps <input type="number" id="sa_exact_turbo_steps" min="1" max="64" step="1" /></label>
|
||||
<label>cfg <input type="number" id="sa_exact_turbo_cfg" min="0.1" max="30" step="0.1" /></label>
|
||||
<label>σ <input type="number" id="sa_exact_turbo_sigma" min="0.1" max="10" step="0.05" /></label>
|
||||
</div>
|
||||
<div class="sa-skills-label">Exact · RAW</div>
|
||||
<div class="sa-settings-row sa-knob-row">
|
||||
<label>steps <input type="number" id="sa_exact_raw_steps" min="1" max="64" step="1" /></label>
|
||||
<label>cfg <input type="number" id="sa_exact_raw_cfg" min="0.1" max="30" step="0.1" /></label>
|
||||
<label>σ <input type="number" id="sa_exact_raw_sigma" min="0.1" max="10" step="0.05" /></label>
|
||||
</div>
|
||||
<div class="sa-settings-row">
|
||||
<button type="button" class="basic-button sa-primary" id="sa_btn_knobs_save">Сохранить knobs</button>
|
||||
<button type="button" class="basic-button" id="sa_btn_reset_ui">Сброс UI-state</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_vision" /> Авто-прикреплять Generate к чату</label>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Авто-Generate после патча</label>
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
|
||||
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
|
||||
</div>
|
||||
|
||||
<div class="sa-view" id="sa_view_chat">
|
||||
|
||||
Reference in New Issue
Block a user