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 {
|
.sa-settings {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
padding: 0.7rem 0.75rem;
|
padding: 0.7rem 0.75rem;
|
||||||
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||||
background: color-mix(in srgb, currentColor 5%, 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 {
|
.sa-settings label {
|
||||||
@@ -744,6 +905,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sa-settings input[type="text"],
|
.sa-settings input[type="text"],
|
||||||
|
.sa-settings input[type="number"],
|
||||||
|
.sa-settings input[type="search"],
|
||||||
|
.sa-settings input[type="range"],
|
||||||
.sa-select {
|
.sa-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
+692
-19
@@ -160,6 +160,9 @@
|
|||||||
slashIndex: 0,
|
slashIndex: 0,
|
||||||
llmParked: false,
|
llmParked: false,
|
||||||
memoryRows: [],
|
memoryRows: [],
|
||||||
|
userPrefs: [],
|
||||||
|
settingsTab: 'behavior',
|
||||||
|
settingsPersonaId: null,
|
||||||
wanted: { count: 0, items: [] },
|
wanted: { count: 0, items: [] },
|
||||||
wantedKeys: new Set(),
|
wantedKeys: new Set(),
|
||||||
ollamaHealth: 'unknown',
|
ollamaHealth: 'unknown',
|
||||||
@@ -2152,6 +2155,18 @@
|
|||||||
fillEmptyParamsFromExact();
|
fillEmptyParamsFromExact();
|
||||||
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
|
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
|
||||||
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
|
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,
|
auto_generate: !!$('sa_auto_generate')?.checked,
|
||||||
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
|
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
|
||||||
model_cards: [],
|
model_cards: [],
|
||||||
taste_profile: summarizeTaste(),
|
user_prefs_count: 0,
|
||||||
...initCtx,
|
...initCtx,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2696,6 +2711,34 @@
|
|||||||
taste.updated = Date.now();
|
taste.updated = Date.now();
|
||||||
state.taste = taste;
|
state.taste = taste;
|
||||||
saveTaste();
|
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.
|
// 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',
|
'prompt', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler',
|
||||||
'actions', 'search_query', 'civitai_query', 'init_creativity', 'denoise',
|
'actions', 'search_query', 'civitai_query', 'init_creativity', 'denoise',
|
||||||
'look_at', 'vision_from', 'vision_slots', 'aspect', 'batch', 'vary', 'lock_seed', 'pack',
|
'look_at', 'vision_from', 'vision_slots', 'aspect', 'batch', 'vary', 'lock_seed', 'pack',
|
||||||
|
'memories', 'user_prefs',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isPatchObject(obj) {
|
function isPatchObject(obj) {
|
||||||
@@ -4265,6 +4309,7 @@
|
|||||||
if (asst.context_prompt_max != null) {
|
if (asst.context_prompt_max != null) {
|
||||||
CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2000);
|
CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2000);
|
||||||
}
|
}
|
||||||
|
fillKnobsFromConfig(data);
|
||||||
if (applyDefaults || data.exact) {
|
if (applyDefaults || data.exact) {
|
||||||
fillEmptyParamsFromExact();
|
fillEmptyParamsFromExact();
|
||||||
}
|
}
|
||||||
@@ -4537,37 +4582,42 @@
|
|||||||
|
|
||||||
function setModelOptions(models, { error } = {}) {
|
function setModelOptions(models, { error } = {}) {
|
||||||
const sel = $('sa_model');
|
const sel = $('sa_model');
|
||||||
if (!sel) {
|
const sel2 = $('sa_settings_chat_model');
|
||||||
|
const apply = (target) => {
|
||||||
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
|
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
|
||||||
sel.innerHTML = '';
|
target.innerHTML = '';
|
||||||
if (error) {
|
if (error) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = '';
|
opt.value = '';
|
||||||
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
|
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
|
||||||
sel.appendChild(opt);
|
target.appendChild(opt);
|
||||||
sel.disabled = true;
|
target.disabled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sel.disabled = false;
|
target.disabled = false;
|
||||||
if (!names.length) {
|
if (!names.length) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = '';
|
opt.value = '';
|
||||||
opt.textContent = 'No Ollama models — pull / Refresh';
|
opt.textContent = 'No Ollama models — pull / Refresh';
|
||||||
sel.appendChild(opt);
|
target.appendChild(opt);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = name;
|
opt.value = name;
|
||||||
opt.textContent = name;
|
opt.textContent = name;
|
||||||
sel.appendChild(opt);
|
target.appendChild(opt);
|
||||||
}
|
}
|
||||||
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
|
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
|
||||||
if (prefer && names.includes(prefer)) {
|
if (prefer && names.includes(prefer)) {
|
||||||
sel.value = prefer;
|
target.value = prefer;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
apply(sel);
|
||||||
|
apply(sel2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEmbedModelOptions(models) {
|
function setEmbedModelOptions(models) {
|
||||||
@@ -4691,6 +4741,14 @@
|
|||||||
return $('sa_mem_kind')?.value || 'all';
|
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) {
|
function renderMemoryKinds(kinds) {
|
||||||
const sel = $('sa_mem_kind');
|
const sel = $('sa_mem_kind');
|
||||||
if (!sel) {
|
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() {
|
function renderMemoryList() {
|
||||||
const root = $('sa_mem_list');
|
const root = $('sa_mem_list');
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filter = memoryKindFilter();
|
const rows = filteredMemoryRows();
|
||||||
const rows = (state.memoryRows || []).filter((m) => filter === 'all' || m.kind === filter);
|
|
||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
if (!rows.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
for (const row of rows) {
|
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) {
|
function modelKeyLeaf(name) {
|
||||||
return String(name || '')
|
return String(name || '')
|
||||||
.replace(/\\/g, '/')
|
.replace(/\\/g, '/')
|
||||||
@@ -4877,6 +5486,7 @@
|
|||||||
el.title = title || text;
|
el.title = title || text;
|
||||||
el.classList.remove('sa-health-ok', 'sa-health-warn', 'sa-health-down');
|
el.classList.remove('sa-health-ok', 'sa-health-warn', 'sa-health-down');
|
||||||
el.classList.add(`sa-health-${level}`);
|
el.classList.add(`sa-health-${level}`);
|
||||||
|
syncSettingsHealthLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
function probeOllamaHealth() {
|
function probeOllamaHealth() {
|
||||||
@@ -5871,7 +6481,7 @@
|
|||||||
} else {
|
} else {
|
||||||
why.push('последнего патча Assistent ещё нет');
|
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 = [
|
const lines = [
|
||||||
'### Debug Assistent',
|
'### Debug Assistent',
|
||||||
@@ -6725,18 +7335,75 @@
|
|||||||
$('sa_btn_settings')?.addEventListener('click', () => {
|
$('sa_btn_settings')?.addEventListener('click', () => {
|
||||||
const s = $('sa_settings');
|
const s = $('sa_settings');
|
||||||
if (s) {
|
if (s) {
|
||||||
s.hidden = !s.hidden;
|
if (s.hidden) {
|
||||||
if (!s.hidden) {
|
openSettings(state.settingsTab || 'behavior');
|
||||||
refreshMemoryList();
|
} else {
|
||||||
refreshWantedQueue();
|
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', () => {
|
$('sa_btn_mem_refresh')?.addEventListener('click', () => {
|
||||||
refreshMemoryList();
|
refreshMemoryList();
|
||||||
refreshWantedQueue();
|
refreshWantedQueue();
|
||||||
});
|
});
|
||||||
$('sa_mem_kind')?.addEventListener('change', renderMemoryList);
|
$('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_btn_look_result')?.addEventListener('click', () => askLookAtResult());
|
||||||
$('sa_ollama_health')?.addEventListener('click', () => probeOllamaHealth());
|
$('sa_ollama_health')?.addEventListener('click', () => probeOllamaHealth());
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
@@ -6746,7 +7413,7 @@
|
|||||||
let closed = false;
|
let closed = false;
|
||||||
const settings = $('sa_settings');
|
const settings = $('sa_settings');
|
||||||
if (settings && !settings.hidden) {
|
if (settings && !settings.hidden) {
|
||||||
settings.hidden = true;
|
closeSettings();
|
||||||
closed = true;
|
closed = true;
|
||||||
}
|
}
|
||||||
if (state.chatsPanelOpen) {
|
if (state.chatsPanelOpen) {
|
||||||
@@ -6854,7 +7521,13 @@
|
|||||||
$('sa_board_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
|
$('sa_board_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
|
||||||
$('sa_clear_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
|
$('sa_clear_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
|
||||||
$('sa_base_url')?.addEventListener('change', saveSettings);
|
$('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', () => {
|
$('sa_embed_model')?.addEventListener('change', () => {
|
||||||
state.preferredEmbed = $('sa_embed_model')?.value || '';
|
state.preferredEmbed = $('sa_embed_model')?.value || '';
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ window.SA = window.SA || {};
|
|||||||
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
||||||
'creativity', 'intensity', 'complexity', 'movement',
|
'creativity', 'intensity', 'complexity', 'movement',
|
||||||
'clear_prompt_images', 'slot_to_prompt_image', 'pack',
|
'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;
|
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);
|
JObject exact = Config.LoadExactForPrompt(pid);
|
||||||
if (exact is not null && exact.Count > 0)
|
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);
|
(reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid);
|
||||||
JObject patch = TryParsePatch(reply);
|
JObject patch = TryParsePatch(reply);
|
||||||
await ApplyMemoryActions(root, patch, embed, pid);
|
await ApplyMemoryActions(root, patch, embed, pid);
|
||||||
|
ApplyUserPrefActions(patch, pid);
|
||||||
ApplyPersonaActions(patch, ref pid);
|
ApplyPersonaActions(patch, ref pid);
|
||||||
if (hop + 1 >= maxHops)
|
if (hop + 1 >= maxHops)
|
||||||
{
|
{
|
||||||
@@ -382,6 +403,19 @@ public partial class SwarmAssistentExtension
|
|||||||
ctx = new JObject { ["_raw_context"] = contextJson };
|
ctx = new JObject { ["_raw_context"] = contextJson };
|
||||||
}
|
}
|
||||||
ctx["memory_hits"] = hits ?? new JArray();
|
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).
|
// Never re-inject full Exact into live context (already in system prompt).
|
||||||
ctx.Remove("exact");
|
ctx.Remove("exact");
|
||||||
if (ctx["session_exact"] is null)
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+249
-1
@@ -715,6 +715,145 @@ public sealed class AssistentConfig
|
|||||||
/// <summary>Exact (KV) memory: bundled base → persona overlays → disk overlays. Persona keys overwrite base.</summary>
|
/// <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));
|
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)
|
public JObject LoadModelProfile(string personaId)
|
||||||
{
|
{
|
||||||
JObject assistant = LoadAssistant(personaId);
|
JObject assistant = LoadAssistant(personaId);
|
||||||
@@ -962,7 +1101,37 @@ public sealed class AssistentConfig
|
|||||||
return char.ToUpperInvariant(key[0]) + key[1..];
|
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";
|
string id = SafeId(personaId) ?? "neutral";
|
||||||
JObject parts = LoadIdentityParts(id);
|
JObject parts = LoadIdentityParts(id);
|
||||||
@@ -976,12 +1145,26 @@ public sealed class AssistentConfig
|
|||||||
sb.AppendLine($"*{tagline}*");
|
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())
|
foreach (JProperty prop in parts.Properties())
|
||||||
{
|
{
|
||||||
if (prop.Name is "persona" or "extra")
|
if (prop.Name is "persona" or "extra")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (allow is not null && !allow.Contains(prop.Name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (prop.Value is not JObject shelf || !shelf.Properties().Any())
|
if (prop.Value is not JObject shelf || !shelf.Properties().Any())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -991,6 +1174,9 @@ public sealed class AssistentConfig
|
|||||||
AppendTokenMarkdown(sb, shelf, 0);
|
AppendTokenMarkdown(sb, shelf, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Controls schema/meanings are fat; Exact already carries current values. Only author/full dump.
|
||||||
|
if (includeAllShelves && onlyShelves is null)
|
||||||
|
{
|
||||||
string controlsBlock = RenderControlsBlock(id);
|
string controlsBlock = RenderControlsBlock(id);
|
||||||
if (!string.IsNullOrWhiteSpace(controlsBlock))
|
if (!string.IsNullOrWhiteSpace(controlsBlock))
|
||||||
{
|
{
|
||||||
@@ -1004,9 +1190,71 @@ public sealed class AssistentConfig
|
|||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(extra.Trim());
|
sb.AppendLine(extra.Trim());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return sb.ToString().TrimEnd();
|
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)
|
static IEnumerable<string> PersonaIdsUnder(string root)
|
||||||
{
|
{
|
||||||
string dir = Path.Combine(root ?? "", "personas");
|
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}");
|
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;
|
_embedModel = GetMeta("embed_model") ?? _embedModel;
|
||||||
_ = int.TryParse(GetMeta("dims"), out _dims);
|
_ = int.TryParse(GetMeta("dims"), out _dims);
|
||||||
_ = int.TryParse(GetMeta("seed_version"), out _seedVersion);
|
_ = 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>
|
/// <summary>The gpu-rent wanted queue (models pending the next <c>up</c>) — count + entries.</summary>
|
||||||
public async Task<JObject> AssistentListWanted(Session session)
|
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",
|
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
|
||||||
"creativity", "intensity", "complexity", "movement",
|
"creativity", "intensity", "complexity", "movement",
|
||||||
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
|
"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)
|
static bool HasValue(JObject obj, string key)
|
||||||
@@ -146,6 +147,18 @@ public partial class SwarmAssistentExtension
|
|||||||
{
|
{
|
||||||
return "lookup_tags";
|
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)))
|
if (ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
|
||||||
{
|
{
|
||||||
return "civitai";
|
return "civitai";
|
||||||
|
|||||||
@@ -139,4 +139,120 @@ public partial class SwarmAssistentExtension
|
|||||||
return new JObject { ["error"] = ex.Message };
|
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_checkpoints_inventory": 60,
|
||||||
"max_wildcards_inventory": 80,
|
"max_wildcards_inventory": 80,
|
||||||
"inventory_blurb_max": 140,
|
"inventory_blurb_max": 140,
|
||||||
|
"inventory_prompt_rich": 12,
|
||||||
|
"inventory_prompt_names": 24,
|
||||||
|
"inventory_hop_limit": 20,
|
||||||
"max_ref_slots": 4,
|
"max_ref_slots": 4,
|
||||||
"default_pack": "write_prompt",
|
"default_pack": "write_prompt",
|
||||||
"default_persona": "neutral",
|
"default_persona": "neutral",
|
||||||
"embed_model": "nomic-embed-text",
|
"embed_model": "nomic-embed-text",
|
||||||
"memory_top_k": 10,
|
"memory_top_k": 8,
|
||||||
|
"memory_hit_chars": 240,
|
||||||
"memory_min_score": 0.32,
|
"memory_min_score": 0.32,
|
||||||
|
"user_prefs_weight": 1.0,
|
||||||
|
"user_prefs_max": 16,
|
||||||
"max_tool_hops": 4,
|
"max_tool_hops": 4,
|
||||||
"tag_lookup_limit": 20,
|
"tag_lookup_limit": 20,
|
||||||
|
"identity_always_shelves": ["persona", "voice", "rules", "likes", "dislikes"],
|
||||||
"memory_quotas": {
|
"memory_quotas": {
|
||||||
"card": 3,
|
"card": 3,
|
||||||
"lora": 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):
|
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.
|
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).
|
3. **About the user** (`## About the user`) — durable preferences (global + this persona). Respect unless this turn overrides.
|
||||||
4. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged into it.
|
4. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat).
|
||||||
5. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change.
|
5. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged.
|
||||||
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).
|
6. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change.
|
||||||
7. Guesses — last resort only.
|
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
|
## 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.
|
- Use only LoRA/checkpoint **names** from `available_loras` / `enabled_loras` (or Civitai hop results). Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**.
|
||||||
- Prefer listed `trigger_phrase` / `triggers` — **never invent** trigger words.
|
- Rich entries (blurbs/triggers) are enabled + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
|
||||||
- `session_exact` / `recommended_params` — session overrides and defaults (Exact KV is in the system block above).
|
- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
|
||||||
- `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` 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.
|
||||||
- `has_vision_image` — if false, do not invent what the image looks like; emit `look_at` first when you need to see it.
|
- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`.
|
||||||
- `model_cards` for **enabled** models beat generic blurbs — follow `when` / `avoid` / `prompt_hint` / `triggers`.
|
- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields (aspect, inpaint, persona authoring) are documented in the active pack.
|
||||||
- `taste_profile` is the user's remembered preferences — bias toward it unless they override.
|
|
||||||
- Prefer `krea_likely` / Krea architecture entries; ignore FLUX/SDXL LoRAs.
|
## Memory (short)
|
||||||
- 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.
|
- Craft RAG write: `memory_upsert` / `memory_forget` + `memories: [{kind,key,text,scope}]` (default personal).
|
||||||
- **Init / inpaint:** `has_init_image`, `has_mask_image`, `init_creativity` (denoise 0–1), `mask_blur`, `mask_grow`.
|
- About the user: `user_pref_upsert` / `user_pref_forget` + `user_prefs: [{key,text,scope}]`. Do **not** put human taste into craft `memories`.
|
||||||
- **Board:** `image_slots`. `generate` = live gen. `ref1`… = refs. Emit `look_at` to see an unattached window.
|
- Fat memory skill text: `skill_load` + `skills: ["memory"]` when you need the full write/read playbook.
|
||||||
|
|
||||||
## Output contract (mandatory)
|
## Output contract (mandatory)
|
||||||
|
|
||||||
1. Write a short helpful reply in the user's language (RU or EN).
|
1. Short helpful reply in the user's language (RU or EN).
|
||||||
2. Then emit **one** fenced JSON patch (only fields you want to change):
|
2. One fenced JSON patch with **only fields you want to change**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"prompt": "...",
|
"prompt": "A fluffy red fox in fresh powder snow, soft morning light, 85mm f/2.8…",
|
||||||
"negative": null,
|
"loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["listed_trigger"]}],
|
||||||
"loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["..."]}],
|
|
||||||
"aspect": "16:9",
|
"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"],
|
"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"
|
"notes": "one-line why"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Patch rules
|
### Patch rules
|
||||||
|
|
||||||
- Omit keys you are not changing.
|
- Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`.
|
||||||
- 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 full intended set for Apply. Prefer `aspect` over raw width/height.
|
||||||
- `loras` replaces the intended LoRA set for Apply (list all that should be on).
|
- 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.
|
||||||
- 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.
|
|
||||||
- Do not invent model or LoRA filenames.
|
- 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).
|
- `"generate"` — Apply + start generation when the user wants a new image.
|
||||||
- `"search_civitai"` — Civitai search; user Confirms downloads.
|
- `"search_civitai"` + `search_query` — Civitai hop (user Confirms downloads).
|
||||||
- `"interrupt"` — stop generation.
|
- `"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"` / `"lookup_tags"` — read hops.
|
||||||
- `"memory_get"` / `"memory_search"` — hop: exact row or hybrid search.
|
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
|
||||||
- `"lookup_tags"` — hop: Danbooru csv (aliases/counts). Do not emit tag soup for Krea.
|
- `"skill_load"` + `skills: ["memory"]` — load fat skill text.
|
||||||
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — author_persona only. Never `"persona_delete"`.
|
- `"persona_read"` — load lore shelves (appearance/outfits/…) not in always-on identity.
|
||||||
- `look_at: ["generate", "ref1"]` — vision hop.
|
- `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes.
|
||||||
- Pure Q&A with no change: omit the JSON patch.
|
- `"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",
|
"id": "memory",
|
||||||
"title": "Exact + vector memory",
|
"title": "Exact + vector memory",
|
||||||
"default": true,
|
"default": false,
|
||||||
"prompt_file": "memory.md"
|
"prompt_file": "memory.md"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
# Skill: memory
|
# 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.
|
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`.
|
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. **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.
|
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
|
## 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)
|
## 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.
|
- `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.
|
- `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.
|
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).
|
- Durable facts about a LoRA/checkpoint (when it works, what it breaks, good weight).
|
||||||
- Bad paths / pitfalls you discovered this session.
|
- 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
|
## When not to write
|
||||||
|
|
||||||
- Do not dump Exact defaults or the full inventory into vector memory.
|
- 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 store the user's taste in craft `memories` — use `user_prefs` instead.
|
||||||
- Do not upsert trivia already in `memory_hits`.
|
- 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.
|
- Do not upsert Danbooru tags — the csv catalog already has them.
|
||||||
- `memory_forget` without `scope` only removes the personal overlay.
|
- `memory_forget` without `scope` only removes the personal overlay.
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Swarm Assistent
|
# 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
|
## 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
|
- **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
|
- **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)
|
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
|
||||||
|
|
||||||
## Config (bundled + overlay)
|
## Config (bundled + overlay)
|
||||||
@@ -26,7 +26,7 @@ Assistent/
|
|||||||
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
||||||
settings.json # embed_model, base_url, per-persona skills
|
settings.json # embed_model, base_url, per-persona skills
|
||||||
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
|
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
|
_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).
|
**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)
|
## 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)
|
- 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)
|
- 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
|
- 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):
|
Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
|
||||||
|
|
||||||
- **Shared** — `Config/_base/memory-seed/`, model cards, `scope: "shared"` upserts. Visible to every persona.
|
- **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.
|
- **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**).
|
- 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 craft notes only — Exact, About the user, and the user beat RAG for params
|
||||||
- Soft notes only — Exact and the user beat RAG for params
|
- ⚙ → **Крафт** lists rows with filters + clear (bundled seed is read-only)
|
||||||
- ⚙ → **Память** lists every row (scope · source · date) with a per-row forget; bundled rows are read-only because reseed brings them back
|
|
||||||
|
|
||||||
## Chats and runtime KV
|
## Chats and runtime KV
|
||||||
|
|
||||||
@@ -116,11 +125,11 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
|||||||
|
|
||||||
## Packs & skills
|
## 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
|
## API routes
|
||||||
|
|
||||||
@@ -133,6 +142,8 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
|||||||
| `AssistentClonePersona` | Snapshot clone → overlay id |
|
| `AssistentClonePersona` | Snapshot clone → overlay id |
|
||||||
| `AssistentSavePersona` | Sparse shelf write (overlay only) |
|
| `AssistentSavePersona` | Sparse shelf write (overlay only) |
|
||||||
| `AssistentDeletePersona` | UI-only delete of overlay persona |
|
| `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) |
|
| `AssistentGetSettings` / `AssistentSaveSettings` | Overlay settings (skills, embed_model) |
|
||||||
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
|
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
|
||||||
| `AssistentListPersonas` | Persona catalog |
|
| `AssistentListPersonas` | Persona catalog |
|
||||||
@@ -140,10 +151,11 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
|||||||
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
|
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
|
||||||
| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
|
| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
|
||||||
| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) |
|
| `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 |
|
| `AssistentSearchCivitai` | Civitai LoRA search |
|
||||||
| `AssistentChat` / `AssistentChatWS` | Chat (+ hybrid memory + Civitai/tag hops) |
|
| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) |
|
||||||
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` | Vector store (optional `scope` / `persona`) |
|
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` / `AssistentClearMemory` | Craft vector store |
|
||||||
| `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key |
|
| `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key |
|
||||||
| `AssistentLookupTags` | Danbooru csv FTS (no embeddings) |
|
| `AssistentLookupTags` | Danbooru csv FTS (no embeddings) |
|
||||||
| `AssistentListChats` / `AssistentGetChat` / `AssistentSaveChat` / `AssistentDeleteChat` | sqlite `chats` (optional `q` FTS) |
|
| `AssistentListChats` / `AssistentGetChat` / `AssistentSaveChat` / `AssistentDeleteChat` | sqlite `chats` (optional `q` FTS) |
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
ExtensionAuthor = "mrleo1nid";
|
ExtensionAuthor = "mrleo1nid";
|
||||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.9.0";
|
Version = "0.10.0";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,15 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
API.RegisterAPICall(AssistentClonePersona, true, PermUse);
|
API.RegisterAPICall(AssistentClonePersona, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentSavePersona, true, PermUse);
|
API.RegisterAPICall(AssistentSavePersona, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentDeletePersona, 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)
|
int CfgInt(string key, int fallback)
|
||||||
|
|||||||
@@ -73,21 +73,107 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="sa-settings" id="sa_settings" hidden>
|
<div class="sa-settings" id="sa_settings" hidden>
|
||||||
|
<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-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>Ollama URL <input type="text" id="sa_base_url" value="http://127.0.0.1:11434" /></label>
|
||||||
<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)">
|
<select id="sa_embed_model" class="sa-select" title="Хранение и группировка памяти (use: memory)">
|
||||||
<option value="nomic-embed-text">nomic-embed-text</option>
|
<option value="nomic-embed-text">nomic-embed-text</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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_models">Обновить модели</button>
|
||||||
<button type="button" class="basic-button" id="sa_btn_refresh_inventory">Обновить inventory</button>
|
<button type="button" class="basic-button" id="sa_btn_refresh_inventory">Обновить inventory</button>
|
||||||
<div class="sa-skills-label">Скилы (процедуры)</div>
|
<button type="button" class="basic-button" id="sa_btn_settings_health">Проверить Ollama</button>
|
||||||
<div class="sa-skills-box" id="sa_skills_box"></div>
|
</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">
|
<div class="sa-mem-head">
|
||||||
<span class="sa-skills-label">Память</span>
|
<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="Фильтр по типу">
|
<select id="sa_mem_kind" class="sa-select sa-mem-kind" title="Фильтр по типу">
|
||||||
<option value="all">Все типы</option>
|
<option value="all">Все типы</option>
|
||||||
</select>
|
</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>
|
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_mem_refresh" title="Перечитать память и очередь wanted">⟳</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-mem-list" id="sa_mem_list"></div>
|
<div class="sa-mem-list" id="sa_mem_list"></div>
|
||||||
@@ -95,11 +181,36 @@
|
|||||||
<span class="sa-mem-total" id="sa_mem_total">Всего: —</span>
|
<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>
|
<span class="sa-mem-wanted" id="sa_mem_wanted" title="Модели в очереди на следующий gpu-rent up">Очередь wanted: —</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_vision" /> Авто-прикреплять Generate к чату</label>
|
<label>memory_top_k <input type="number" id="sa_memory_top_k" min="1" max="30" step="1" value="10" /></label>
|
||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
|
<div class="sa-settings-row">
|
||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Авто-Generate после патча</label>
|
<button type="button" class="basic-button" id="sa_btn_mem_clear_kind">Очистить тип</button>
|
||||||
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
|
<button type="button" class="basic-button" id="sa_btn_mem_clear_persona">Очистить личность</button>
|
||||||
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sa-view" id="sa_view_chat">
|
<div class="sa-view" id="sa_view_chat">
|
||||||
|
|||||||
Reference in New Issue
Block a user