Add agency

This commit is contained in:
dimitrievgs 2026-05-30 23:47:16 +03:00
parent 768d93700e
commit 0acd67ee57
8 changed files with 668 additions and 287 deletions

622
main.ts
View File

@ -7,6 +7,8 @@ import { TemplateEngine } from './src/references/TemplateEngine';
import { AutocompleteManager } from './src/references/AutocompleteManager';
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
import { ReferenceParser } from './src/references/ReferenceParser';
import { CustomTool } from './src/components/models/ChatHistoryItem';
import { v4 as uuidv4 } from 'uuid';
import 'src/css/styles.css';
@ -19,6 +21,7 @@ interface LLMAgentSettings {
systemPrompt: string;
defaultModel: string;
promptsFolder: string;
scriptsFolder: string; // Новое: Папка со скриптами питона
enableFileReferences: boolean;
enablePromptReferences: boolean;
excludedFolders: string[];
@ -26,6 +29,8 @@ interface LLMAgentSettings {
temperatureChat: number;
maxTokensChatDefault: string;
agencyModeEnabled: boolean; // Новое: По умолчанию включен ли агент
customTools: CustomTool[]; // Новое: Список созданных инструментов
summarizationModel: string;
voiceCommandModel: string;
@ -57,6 +62,7 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
systemPrompt: 'Ты полезный ИИ ассистент.',
defaultModel: LLM_DEFAULTS.MODEL,
promptsFolder: 'prompts',
scriptsFolder: 'scripts',
enableFileReferences: true,
enablePromptReferences: true,
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'],
@ -64,6 +70,8 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
temperatureChat: LLM_DEFAULTS.TEMPERATURE,
maxTokensChatDefault: '',
agencyModeEnabled: true,
customTools: [],
summarizationModel: '',
voiceCommandModel: '',
@ -112,6 +120,23 @@ export default class LLMAgentPlugin extends Plugin {
settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands);
settingsToSync.promptMetrics = await engine.processRawSettingValue(this.settings.promptMetrics);
// Раскрываем скрытые промпты у всех тулов
for (let i = 0; i < settingsToSync.customTools.length; i++) {
if (settingsToSync.customTools[i].hiddenPrompt) {
settingsToSync.customTools[i].hiddenPromptExpanded = await engine.processRawSettingValue(settingsToSync.customTools[i].hiddenPrompt);
} else {
settingsToSync.customTools[i].hiddenPromptExpanded = "";
}
// Конвертируем относительный путь скрипта в абсолютный для бэкенда
const adapter = this.app.vault.adapter;
if (adapter instanceof FileSystemAdapter && settingsToSync.customTools[i].scriptPath) {
const vaultPath = adapter.getBasePath();
const cleanScriptPath = settingsToSync.customTools[i].scriptPath.replace('@', '').trim();
settingsToSync.customTools[i].scriptPath = `${vaultPath}/${cleanScriptPath}`;
}
}
const adapter = this.app.vault.adapter;
if (adapter instanceof FileSystemAdapter) {
// Соединяем базовый путь хранилища и относительный путь к логам
@ -125,7 +150,7 @@ export default class LLMAgentPlugin extends Plugin {
body: JSON.stringify(settingsToSync)
});
if (response.ok) {
console.log("LLM Agent: Settings synced with backend.");
console.log("LLM Agent: Settings and Tools synced with backend.");
}
} catch (e) {
console.error("LLM Agent: Failed to sync settings", e);
@ -302,6 +327,7 @@ export default class LLMAgentPlugin extends Plugin {
class SampleSettingTab extends PluginSettingTab {
plugin: LLMAgentPlugin;
activeTabId: string = 'basic';
constructor(app: App, plugin: LLMAgentPlugin) {
super(app, plugin);
@ -312,9 +338,68 @@ class SampleSettingTab extends PluginSettingTab {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Основные настройки' });
// Навигация (Табы)
const tabHeader = containerEl.createDiv({ cls: 'settings-tab-header' });
tabHeader.style.cssText = 'display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid var(--background-modifier-border); padding-bottom: 10px;';
new Setting(containerEl)
const tabs = [
{ id: 'basic', label: '⚙️ Основные' },
{ id: 'agency', label: '🤖 Агент (ReAct Tools)' },
{ id: 'voice', label: '🎤 Голос (Vosk)' }
];
const contentContainer = containerEl.createDiv({ cls: 'settings-tab-content' });
const renderTab = (id: string) => {
contentContainer.empty();
// Подсветка активной кнопки
Array.from(tabHeader.children).forEach((child: HTMLElement) => {
if (child.dataset.tabId === id) {
child.style.backgroundColor = 'var(--interactive-accent)';
child.style.color = 'var(--text-on-accent)';
} else {
child.style.backgroundColor = 'var(--background-secondary)';
child.style.color = 'var(--text-normal)';
}
});
if (id === 'basic') this.renderBasicSettings(contentContainer);
if (id === 'agency') this.renderAgencySettings(contentContainer);
if (id === 'voice') this.renderVoiceSettings(contentContainer);
};
tabs.forEach(tab => {
const btn = tabHeader.createEl('button', { text: tab.label });
btn.dataset.tabId = tab.id;
btn.style.cssText = 'border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; transition: 0.2s;';
btn.onclick = () => {
this.activeTabId = tab.id;
renderTab(this.activeTabId);
};
});
renderTab(this.activeTabId);
}
renderBasicSettings(container: HTMLElement) {
container.createEl('h3', { text: 'Глобальные параметры' });
new Setting(container)
.setName('API Base URL')
.setDesc('Базовый URL для API LLM Agent')
.addText((text) =>
text
.setPlaceholder('http://localhost:5000/api')
.setValue(this.plugin.settings.apiBaseUrl)
.onChange(async (value) => {
this.plugin.settings.apiBaseUrl = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Модель по умолчанию')
.setDesc('Выберите модель для основного чата.')
.addDropdown((dropdown) => {
@ -327,7 +412,7 @@ class SampleSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl)
new Setting(container)
.setName('Модель суммаризации')
.setDesc('Выберите модель для суммаризации.')
.addDropdown((dropdown) => {
@ -340,10 +425,7 @@ class SampleSettingTab extends PluginSettingTab {
});
});
// Этот промпт будет отправляться LLM перед каждым диалогом
this.addPromptAutocomplete(new Setting(containerEl).setName('Системный промпт'), 'systemPrompt');
new Setting(containerEl)
new Setting(container)
.setName('Температура чата по умолчанию')
.setDesc(`Значение от 0.0 до 2.0 (по умолчанию ${LLM_DEFAULTS.TEMPERATURE}). Влияет на креативность.`)
.addText((text) =>
@ -358,7 +440,7 @@ class SampleSettingTab extends PluginSettingTab {
})
);
new Setting(containerEl)
new Setting(container)
.setName('Макс. токенов чата по умолчанию')
.setDesc('Оставьте пустым для отсутствия лимита.')
.addText((text) =>
@ -372,220 +454,12 @@ class SampleSettingTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.setName('API Base URL')
.setDesc('Базовый URL для API LLM Agent')
.addText((text) =>
text
.setPlaceholder('http://localhost:5000/api')
.setValue(this.plugin.settings.apiBaseUrl)
.onChange(async (value) => {
this.plugin.settings.apiBaseUrl = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
// Этот промпт будет отправляться LLM перед каждым диалогом
this.addPromptAutocomplete(new Setting(container).setName('Глобальный системный промпт'), 'systemPrompt');
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
container.createEl('h3', { text: 'Управление ссылками и файлами' });
new Setting(containerEl)
.setName('Отображать голосовую панель')
.setDesc('Показывать нижнюю панель Voice Service в графе диалогов.')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showVoicePanel).onChange(async (value) => {
this.plugin.settings.showVoicePanel = value;
await this.plugin.saveSettings();
// Событие 'settings-changed' уже вызывается внутри saveSettings,
// поэтому панель будет скрываться/появляться мгновенно
})
);
new Setting(containerEl)
.setName('Модель команд')
.setDesc('LLM для обработки голосовых команд.')
.addDropdown((dropdown) => {
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
dropdown.setValue(this.plugin.settings.voiceCommandModel)
.onChange(async (value) => {
this.plugin.settings.voiceCommandModel = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
});
});
// Инструкции для Промпта №1.
this.addPromptAutocomplete(
new Setting(containerEl).setName('Регулярный промпт'),
'promptRegular'
);
// Инструкции для Промпта №2.
this.addPromptAutocomplete(
new Setting(containerEl).setName('Промпт команд'),
'promptCommands'
);
this.addPromptAutocomplete(
new Setting(containerEl).setName('Промпт качественной аналитики'),
'promptMetrics'
);
new Setting(containerEl)
.setName('Температура Voice')
.setDesc(`Температура для голосовых запросов (по умолчанию ${LLM_DEFAULTS.TEMPERATURE})`)
.addText((text) =>
text
.setPlaceholder(String(LLM_DEFAULTS.TEMPERATURE))
.setValue(String(this.plugin.settings.temperatureVoice))
.onChange(async (value) => {
const num = parseFloat(value);
this.plugin.settings.temperatureVoice = isNaN(num) ? LLM_DEFAULTS.TEMPERATURE : num;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Макс. токенов Voice (Регулярный)')
.setDesc('Лимит токенов для регулярного анализа. Оставьте пустым для безлимита.')
.addText((text) =>
text
.setPlaceholder('Лимит')
.setValue(this.plugin.settings.maxTokensVoiceRegular)
.onChange(async (value) => {
this.plugin.settings.maxTokensVoiceRegular = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Макс. токенов Voice (Команды)')
.setDesc('Лимит токенов для голосовых команд. Оставьте пустым для безлимита.')
.addText((text) =>
text
.setPlaceholder('Лимит')
.setValue(this.plugin.settings.maxTokensVoiceCommands)
.onChange(async (value) => {
this.plugin.settings.maxTokensVoiceCommands = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Интервал регулярного анализа (мин)')
.setDesc('Как часто LLM анализирует последние фразы (Prompt 1).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.voiceInterval))
.onChange(async (value) => {
this.plugin.settings.voiceInterval = Number(value);
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Интервал аналитики (мин)')
.setDesc('Как часто Python и LLM генерируют графики и качественный анализ (Таб 4).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.metricsInterval))
.onChange(async (value) => {
this.plugin.settings.metricsInterval = Number(value) || 15;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Действие диктовки')
.setDesc('При нажатии записи в чате, если голос не запущен, как его стартовать?')
.addDropdown((dropdown) => {
dropdown.addOption('start', 'Начать заново (Start New)');
dropdown.addOption('continue', 'Продолжить (Continue)');
dropdown.setValue(this.plugin.settings.dictationStartAction)
.onChange(async (value: 'start' | 'continue') => {
this.plugin.settings.dictationStartAction = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
});
});
new Setting(containerEl)
.setName('Папка аудио-логов')
.setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.')
.addText((text) =>
text
.setPlaceholder('audio-logs')
.setValue(this.plugin.settings.logsFolder)
.onChange(async (value) => {
// Убираем лишние слэши для корректности пути
this.plugin.settings.logsFolder = value.replace(/^\/+|\/+$/g, '').trim();
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Объем контекста (символы)')
.setDesc('Сколько последних символов транскрибации отправлять в модель.')
.addText((text) =>
text
.setValue(String(this.plugin.settings.voiceContextLimit))
.onChange(async (value) => {
this.plugin.settings.voiceContextLimit = Number(value);
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Маркер начала команды')
.setDesc('Слово, после которого текст считается командой (Response 2).')
.addText((text) =>
text
.setPlaceholder('старт')
.setValue(this.plugin.settings.markerStart)
.onChange(async (value) => {
this.plugin.settings.markerStart = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Маркер конца команды')
.setDesc('Слово, завершающее захват голосовой команды.')
.addText((text) =>
text
.setPlaceholder('стоп')
.setValue(this.plugin.settings.markerStop)
.onChange(async (value) => {
this.plugin.settings.markerStop = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Путь к модели Vosk')
.setDesc('Абсолютный путь к папке с распакованной языковой моделью (например: C:\\vosk-models\\vosk-model-small-ru)')
.addText((text) =>
text
.setPlaceholder('Путь к модели на диске')
.setValue(this.plugin.settings.voskModelPath)
.onChange(async (value) => {
this.plugin.settings.voskModelPath = value.replace(/"/g, '').trim(); // Убираем кавычки, если скопировали путь как путь
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
new Setting(containerEl)
new Setting(container)
.setName('Папка с промптами')
.setDesc('Путь к папке, содержащей промпты для команд #/##')
.addText((text) =>
@ -599,7 +473,7 @@ class SampleSettingTab extends PluginSettingTab {
})
);
new Setting(containerEl)
new Setting(container)
.setName('Папка кеша для вложений')
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
.addText((text) =>
@ -614,7 +488,7 @@ class SampleSettingTab extends PluginSettingTab {
);
// Настройки ссылок на файлы
new Setting(containerEl)
new Setting(container)
.setName('Включить ссылки на файлы')
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
.addToggle((toggle) =>
@ -626,7 +500,7 @@ class SampleSettingTab extends PluginSettingTab {
);
// Настройки ссылок на промпты
new Setting(containerEl)
new Setting(container)
.setName('Включить ссылки на промпты')
.setDesc('Разрешить использование #/## ссылок на промпты')
.addToggle((toggle) =>
@ -638,7 +512,7 @@ class SampleSettingTab extends PluginSettingTab {
);
// Новое поле для настройки исключенных папок
new Setting(containerEl)
new Setting(container)
.setName('Исключенные папки для ссылок')
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
.addToggle((toggle) =>
@ -650,9 +524,9 @@ class SampleSettingTab extends PluginSettingTab {
);
// Динамический список исключенных папок
containerEl.createEl('h3', { text: 'Исключенные папки' });
container.createEl('h3', { text: 'Исключенные папки' });
this.plugin.settings.excludedFolders.forEach((folder, index) => {
new Setting(containerEl)
new Setting(container)
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
.addText((text) => {
text.setPlaceholder('Путь к папке (например, templates/)' + index)
@ -680,7 +554,7 @@ class SampleSettingTab extends PluginSettingTab {
});
});
new Setting(containerEl).addButton((button) => {
new Setting(container).addButton((button) => {
button
.setButtonText('Добавить папку')
.setIcon('plus')
@ -693,6 +567,298 @@ class SampleSettingTab extends PluginSettingTab {
});
}
renderAgencySettings(container: HTMLElement) {
container.createEl('h3', { text: 'Фреймворк Инструментов (ReAct Engine)' });
new Setting(container)
.setName('Агентность включена по умолчанию')
.setDesc('Разрешает ИИ вызывать ваши питоновские скрипты по мере необходимости')
.addToggle((t) => t.setValue(this.plugin.settings.agencyModeEnabled).onChange(async (v) => {
this.plugin.settings.agencyModeEnabled = v;
await this.plugin.saveSettings();
}));
container.createEl('h4', { text: 'Кастомные инструменты (Mini-Agents)', cls: 'agency-tools-header' });
if (!this.plugin.settings.customTools) this.plugin.settings.customTools = [];
this.plugin.settings.customTools.forEach((tool, index) => {
const toolDiv = container.createDiv({
attr: { style: 'border: 1px solid var(--interactive-accent); padding: 10px; margin-bottom: 15px; border-radius: 8px; background-color: var(--background-secondary-alt);' }
});
new Setting(toolDiv)
.setName(`Название (только англ.)`)
.addText(t => t.setValue(tool.name).onChange(async v => {
tool.name = v.replace(/[^a-zA-Z0-9_]/g, '');
await this.plugin.saveSettings();
}))
.addExtraButton(b => b.setIcon('trash').setTooltip('Удалить этот инструмент').onClick(async () => {
this.plugin.settings.customTools.splice(index, 1);
await this.plugin.saveSettings();
this.display(); // Полностью перерисовать
}));
new Setting(toolDiv)
.setName('Описание для ИИ (Когда применять?)')
.addTextArea(t => {
t.inputEl.style.width = '100%';
t.setValue(tool.description).onChange(async v => {
tool.description = v;
await this.plugin.saveSettings();
});
});
const scriptSetting = new Setting(toolDiv)
.setName('Скрипт и Метод')
.setDesc('путь/скрипт.py:названиеетода (Используйте @)');
this.addPromptAutocompleteStrict(scriptSetting, tool, 'scriptPath', 'py');
const formatSetting = new Setting(toolDiv)
.setName('Форматирующий промпт (Пост-процессинг)')
.setDesc('Стиль ответа, прикладываемый к результату работы скрипта (Используйте #)');
this.addPromptAutocompleteStrict(formatSetting, tool, 'hiddenPrompt');
});
new Setting(container).addButton(b => b.setButtonText(' Добавить новый инструмент').setCta().onClick(async () => {
this.plugin.settings.customTools.push({ id: uuidv4(), name: 'New_Tool', description: '', scriptPath: '', hiddenPrompt: '' });
await this.plugin.saveSettings();
this.display();
}));
}
renderVoiceSettings(container: HTMLElement) {
container.createEl('h3', { text: 'Транскрибация Vosk' });
new Setting(container)
.setName('Отображать голосовую панель')
.setDesc('Показывать нижнюю панель Voice Service в графе диалогов.')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.showVoicePanel).onChange(async (value) => {
this.plugin.settings.showVoicePanel = value;
await this.plugin.saveSettings();
// Событие 'settings-changed' уже вызывается внутри saveSettings,
// поэтому панель будет скрываться/появляться мгновенно
})
);
new Setting(container)
.setName('Путь к модели Vosk')
.setDesc('Абсолютный путь к папке с распакованной языковой моделью (например: C:\\vosk-models\\vosk-model-small-ru)')
.addText((text) =>
text
.setPlaceholder('Путь к модели на диске')
.setValue(this.plugin.settings.voskModelPath)
.onChange(async (value) => {
this.plugin.settings.voskModelPath = value.replace(/"/g, '').trim(); // Убираем кавычки, если скопировали путь как путь
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Модель команд')
.setDesc('LLM для обработки голосовых команд.')
.addDropdown((dropdown) => {
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
dropdown.setValue(this.plugin.settings.voiceCommandModel)
.onChange(async (value) => {
this.plugin.settings.voiceCommandModel = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
});
});
new Setting(container)
.setName('Температура Voice')
.setDesc(`Температура для голосовых запросов (по умолчанию ${LLM_DEFAULTS.TEMPERATURE})`)
.addText((text) =>
text
.setPlaceholder(String(LLM_DEFAULTS.TEMPERATURE))
.setValue(String(this.plugin.settings.temperatureVoice))
.onChange(async (value) => {
const num = parseFloat(value);
this.plugin.settings.temperatureVoice = isNaN(num) ? LLM_DEFAULTS.TEMPERATURE : num;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Макс. токенов Voice (Регулярный)')
.setDesc('Лимит токенов для регулярного анализа. Оставьте пустым для безлимита.')
.addText((text) =>
text
.setPlaceholder('Лимит')
.setValue(this.plugin.settings.maxTokensVoiceRegular)
.onChange(async (value) => {
this.plugin.settings.maxTokensVoiceRegular = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Макс. токенов Voice (Команды)')
.setDesc('Лимит токенов для голосовых команд. Оставьте пустым для безлимита.')
.addText((text) =>
text
.setPlaceholder('Лимит')
.setValue(this.plugin.settings.maxTokensVoiceCommands)
.onChange(async (value) => {
this.plugin.settings.maxTokensVoiceCommands = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Действие диктовки')
.setDesc('При нажатии записи в чате, если голос не запущен, как его стартовать?')
.addDropdown((dropdown) => {
dropdown.addOption('start', 'Начать заново (Start New)');
dropdown.addOption('continue', 'Продолжить (Continue)');
dropdown.setValue(this.plugin.settings.dictationStartAction)
.onChange(async (value: 'start' | 'continue') => {
this.plugin.settings.dictationStartAction = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
});
});
new Setting(container)
.setName('Папка аудио-логов')
.setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.')
.addText((text) =>
text
.setPlaceholder('audio-logs')
.setValue(this.plugin.settings.logsFolder)
.onChange(async (value) => {
// Убираем лишние слэши для корректности пути
this.plugin.settings.logsFolder = value.replace(/^\/+|\/+$/g, '').trim();
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Объем контекста (символы)')
.setDesc('Сколько последних символов транскрибации отправлять в модель.')
.addText((text) =>
text
.setValue(String(this.plugin.settings.voiceContextLimit))
.onChange(async (value) => {
this.plugin.settings.voiceContextLimit = Number(value);
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Маркер начала команды')
.setDesc('Слово, после которого текст считается командой (Response 2).')
.addText((text) =>
text
.setPlaceholder('старт')
.setValue(this.plugin.settings.markerStart)
.onChange(async (value) => {
this.plugin.settings.markerStart = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Маркер конца команды')
.setDesc('Слово, завершающее захват голосовой команды.')
.addText((text) =>
text
.setPlaceholder('стоп')
.setValue(this.plugin.settings.markerStop)
.onChange(async (value) => {
this.plugin.settings.markerStop = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
container.createEl('h3', { text: 'Интервалы и Аналитика' });
new Setting(container)
.setName('Интервал регулярного анализа (мин)')
.setDesc('Как часто LLM анализирует последние фразы (Prompt 1).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.voiceInterval))
.onChange(async (value) => {
this.plugin.settings.voiceInterval = Number(value);
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(container)
.setName('Интервал аналитики (мин)')
.setDesc('Как часто Python и LLM генерируют графики и качественный анализ (Таб 4).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.metricsInterval))
.onChange(async (value) => {
this.plugin.settings.metricsInterval = Number(value) || 15;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
container.createEl('h3', { text: 'Промпты голоса' });
this.addPromptAutocomplete(new Setting(container).setName('Регулярный промпт'), 'promptRegular');
this.addPromptAutocomplete(new Setting(container).setName('Промпт команд (Marker)'), 'promptCommands');
this.addPromptAutocomplete(new Setting(container).setName('Промпт Качественной Аналитики'), 'promptMetrics');
}
private addPromptAutocompleteStrict(setting: Setting, obj: any, key: string, filterExtension?: string) {
const manager = new AutocompleteManager(this.plugin);
setting.addText((text) => {
const el = text.inputEl;
const autocomplete = new ReferenceAutocomplete(el, manager);
autocomplete.onItemSelected(async (item, match) => {
const val = el.value;
const before = val.substring(0, match.startIndex);
const after = val.substring(match.endIndex);
// ИЗМЕНЕНО: Если это файл (скрипт), берем ВЕСЬ ПУТЬ к нему, а не просто Имя.
// Это позволяет бэкенду выполнять скрипты из подпапок (напр. scripts/my_script.py)
const textToUse = item.type === 'file' ? item.path : item.name;
const nameToInsert = textToUse.includes(' ') ? `"${textToUse}"` : textToUse;
const prefix = item.type === 'file' ? '@' : '#';
const newValue = before + prefix + nameToInsert + after;
el.value = newValue;
obj[key] = newValue;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
autocomplete.hideAutocomplete();
});
text.setValue(String(obj[key])).onChange(async (value) => {
obj[key] = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
const cursor = el.selectionStart;
const ref = ReferenceParser.getCurrentReference(el.value, cursor);
if (ref) {
const rect = el.getBoundingClientRect();
// ИЗМЕНЕНО: Отправляем фильтр расширения (если он был задан)
autocomplete.showAutocomplete(ref, rect, filterExtension);
} else {
autocomplete.hideAutocomplete();
}
});
el.addEventListener('keydown', (e) => { if (autocomplete.isVisible() && autocomplete.handleKeydown(e)) { e.stopPropagation(); } });
});
}
private addPromptAutocomplete(setting: Setting, settingKey: keyof LLMAgentSettings) {
const manager = new AutocompleteManager(this.plugin);

View File

@ -20,7 +20,7 @@ import { LLM_DEFAULTS } from 'src/constants';
interface ChatPanelProps {
chatHistory: ChatHistoryItem[];
onSendMessage: (message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number) => void;
onSendMessage: (message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number, agencyMode?: boolean) => void;
selectedModel?: string;
plugin: LLMAgentPlugin;
currentGraphCustomSystemPrompt: string | null;
@ -64,6 +64,8 @@ export class ChatPanel {
dictationLastLen: number = 0;
hasGlobalStreams: boolean = false;
agencyToggle: HTMLInputElement;
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
cacheManager: CacheManager;
@ -211,15 +213,13 @@ export class ChatPanel {
this.modelDropdown.style.display = 'none';
}
handleInput(): void {
const value = this.messageInput.textContent || '';
const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat'];
if (value.startsWith('/')) {
const filteredCommands = commands.filter((cmd) => cmd.startsWith(value.toLowerCase()));
this.showSuggestions(filteredCommands);
} else {
this.hideSuggestions();
async setAgencyToggle(): Promise<void> {
if (this.props.plugin.settings) {
this.props.plugin.settings.agencyModeEnabled = this.agencyToggle.checked;
if (typeof this.props.plugin.saveSettings === 'function') {
await this.props.plugin.saveSettings(); // TODO: Подумать, насколько это должно быть здесь
await this.props.plugin.syncSettings();
}
}
}
@ -467,6 +467,45 @@ export class ChatPanel {
this.processStreamUpdates();
}
toolExecutionUpdate(nodeId: string, toolName: string, status: 'start' | 'end', step: number = 1): void {
const messageEl = this.container.querySelector(`.message[data-node-id="${nodeId}"]`);
if (!messageEl) return;
const contentDiv = messageEl.querySelector('.chat-message-content') as HTMLElement;
let indicator = contentDiv.querySelector('.agent-steps-info');
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'agent-steps-info';
// Вставляем В НАЧАЛО контента
contentDiv.insertBefore(indicator, contentDiv.firstChild);
}
if (status === 'start') {
indicator.innerHTML = `
<div class="step-badge">
🛠 Шаг ${step}: Подготовка и вызов <b>${toolName}</b>...
</div>
`;
} else if (status === 'end') {
indicator.innerHTML = `
<div class="step-badge success">
Шаг ${step}: Инструмент <b>${toolName}</b> успешно завершен
</div>
`;
// Прячем значок успеха через 3.5 секунды, чтобы не мозолил глаза
setTimeout(() => {
if (indicator && indicator.parentNode) {
indicator.remove();
}
}, 3500);
}
// Автоскролл
const scrollContainer = this.container.closest('.view-content') as HTMLElement;
if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
/**
* Обрабатывает очередь так, чтобы рендер происходил не чаще чем раз в 300мс
* и дожидался окончания предыдущей отрисовки.
@ -614,6 +653,15 @@ export class ChatPanel {
</span>
</div>
<div class="agency-toggle-container" title="Включить Агентность (Вызов скриптов)">
<label class="switch">
<input type="checkbox" id="agency-toggle" ${this.props.plugin.settings.agencyModeEnabled ? 'checked' : ''}>
<span class="slider"></span>
</label>
<span>🤖 Агент</span>
</div>
<div class="param-select-container" title="Температура">
<span class="param-icon">Temp: </span>
<input type="number" id="temp-input" class="param-input" placeholder="${LLM_DEFAULTS.TEMPERATURE}" min="0" max="2" step="0.1">
@ -657,6 +705,8 @@ export class ChatPanel {
this.attachedFilesContainer = this.container.querySelector('#attached-files-container') as HTMLDivElement;
this.agencyToggle = this.container.querySelector('#agency-toggle') as HTMLInputElement;
this.tempInput = this.container.querySelector('#temp-input') as HTMLInputElement;
this.tempResetBtn = this.container.querySelector('#temp-reset') as HTMLButtonElement;
this.maxTokensInput = this.container.querySelector('#max-tokens-input') as HTMLInputElement;
@ -723,7 +773,6 @@ export class ChatPanel {
});
this.messageInput.addEventListener('input', () => {
this.handleInput();
this.handleReferenceInput();
this.updatePlaceholder();
});
@ -741,6 +790,10 @@ export class ChatPanel {
this.toggleModelDropdown();
});
this.stopButton.addEventListener('click', () => this.handleStopGeneration());
this.maxTokensResetBtn.addEventListener('click', () => {
@ -766,6 +819,34 @@ export class ChatPanel {
this.updateScrollButtonsVisibility();
});
this.agencyToggle.addEventListener('change', async () => {
this.setAgencyToggle();
});
// ???
this.props.plugin.eventBus.on('agent-step', (data: { nodeId: string, step: number, tool: string, status: string }) => {
// Ищем контейнер сообщения по его ID (тот, что сейчас стримится)
const messageEl = this.container.querySelector(`.message[data-node-id="${data.nodeId}"]`);
if (!messageEl) return;
let indicator = messageEl.querySelector('.agent-steps-info') as HTMLElement;
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'agent-steps-info';
// Вставляем В НАЧАЛО контента сообщения
const content = messageEl.querySelector('.chat-message-content');
content?.prepend(indicator);
}
if (data.status === 'start') {
indicator.innerHTML = `<div class="step-badge">🛠 Шаг ${data.step}: Выполняю <b>${data.tool}</b>...</div>`;
} else if (data.status === 'end') {
indicator.innerHTML = `<div class="step-badge success">✅ Шаг ${data.step}: ${data.tool} завершен</div>`;
// Через 3 секунды можно скрыть индикатор шага, чтобы не мешался
setTimeout(() => indicator.remove(), 3000);
}
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
@ -1136,7 +1217,9 @@ export class ChatPanel {
const valTemp = this.tempInput?.value;
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
this.props.onSendMessage(originalText || '', this.selectedModel, allAttachments, parsedMaxTokens, parsedTemp);
const isAgencyActive = this.agencyToggle.checked;
this.props.onSendMessage(originalText || '', this.selectedModel, allAttachments, parsedMaxTokens, parsedTemp, isAgencyActive);
}
this.clearInput();
@ -1174,7 +1257,9 @@ export class ChatPanel {
const valTemp = this.tempInput?.value;
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
this.props.onSendMessage(message, this.selectedModel, this.currentAttachments, parsedMaxTokens, parsedTemp);
const isAgencyActive = this.agencyToggle.checked;
this.props.onSendMessage(message, this.selectedModel, this.currentAttachments, parsedMaxTokens, parsedTemp, isAgencyActive);
this.clearInput();
}
}
@ -1261,12 +1346,15 @@ export class ChatPanel {
const valTemp = this.tempInput?.value;
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
const isAgencyActive = this.agencyToggle.checked;
this.props.plugin.eventBus.emit('regenerate-message', {
graphId,
nodeId,
model: this.selectedModel,
maxTokens: parsedMaxTokens,
temperature: parsedTemp
temperature: parsedTemp,
agencyMode: isAgencyActive
});
});

View File

@ -105,13 +105,15 @@ const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ data, isConnectable,
// или перелопачивать код и хранить в узлах и бд настройки, использованные при генерации, как будто, ещё хуже
let maxTokens = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.maxTokensChatDefault || '';
let temperature = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.temperatureChat || LLM_DEFAULTS.TEMPERATURE;
let agencyMode = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.agencyModeEnabled || false;
eventBus.emit('regenerate-message', {
graphId: data.graphId,
nodeId: data.nodeId,
model: model, // НОВОЕ: Добавляем модель
maxTokens: maxTokens,
temperature: temperature
temperature: temperature,
agencyMode: agencyMode
});
}
};

View File

@ -21,3 +21,12 @@ export interface Attachment {
permanent: boolean; // true для !@, !#, !$
mimeType?: string;
}
export interface CustomTool {
id: string;
name: string;
description: string;
scriptPath: string;
hiddenPrompt: string;
hiddenPromptExpanded?: string;
}

View File

@ -309,22 +309,17 @@
.chat-controls {
display: flex;
flex-wrap: wrap; /* Элементы переносятся, если панель узкая */
gap: 6px;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-muted);
padding-top: 4px;
}
.control-buttons-left {
.control-buttons-left, .control-buttons-right {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
gap: 8px;
}
.control-buttons-right {
display: flex;
align-items: center;
gap: 8px;
}
.model-select-container {
@ -1660,3 +1655,98 @@
100% { transform: scale(1); opacity: 1; }
}
/* ----------------------------------------------- Agency toggle --------------------------------------------------------------- */
.agency-toggle-container {
display: flex;
align-items: center;
gap: 6px;
margin-right: 4px;
font-size: 11px;
color: var(--text-muted);
}
.switch {
position: relative;
display: inline-block;
width: 28px;
height: 16px;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: var(--background-modifier-border);
transition: .2s;
border-radius: 16px;
}
.slider:before {
position: absolute;
content: "";
height: 12px; width: 12px;
left: 2px; bottom: 2px;
background-color: white;
transition: .2s;
border-radius: 50%;
}
input:checked + .slider { background-color: var(--interactive-accent); }
input:checked + .slider:before { transform: translateX(12px); }
/* Визуальный блок работы инструмента (Scratchpad-нода) */
.tool-execution-block {
margin: 8px 0;
padding: 8px;
border-radius: 6px;
background-color: var(--background-secondary-alt);
border-left: 3px solid var(--interactive-accent);
font-family: var(--font-monospace);
font-size: 0.85em;
color: var(--text-muted);
animation: pulse-bg 1.5s infinite;
}
.tool-execution-block.finished {
animation: none;
border-left: 3px solid var(--text-success);
opacity: 0.8;
}
@keyframes pulse-bg {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
/* --- Индикаторы работы Агента (ReAct Steps) --- */
.agent-steps-info {
margin-bottom: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.step-badge {
display: inline-flex;
padding: 6px 10px;
background-color: var(--background-secondary-alt);
border-left: 3px solid var(--interactive-accent);
border-radius: 4px;
font-family: var(--font-monospace);
font-size: 0.85em;
color: var(--text-muted);
animation: pulse-bg 1.5s infinite;
}
.step-badge.success {
animation: none;
border-left-color: var(--text-success);
opacity: 0.8;
}
@keyframes pulse-bg {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}

View File

@ -43,20 +43,22 @@ export class AutocompleteManager {
/**
* Получает список файлов для автокомплита
* @param query - поисковый запрос
* @param filterExtension - опциональный фильтр по расширению (например, 'py')
* @returns массив файлов для автокомплита
*/
async getFileOptions(query: string): Promise<AutocompleteItem[]> {
async getFileOptions(query: string, filterExtension?: string): Promise<AutocompleteItem[]> {
if (!this.plugin.settings.enableFileReferences) return [];
const lowercaseQuery = query.toLowerCase();
const virtualItems: AutocompleteItem[] = [];
// Пункт 1: Если в запросе "cur", добавляем логи в начало
if ("current-log".includes(lowercaseQuery)) {
// (Только если мы не ищем скрипты конкретного расширения)
if (!filterExtension && "current-log".includes(lowercaseQuery)) {
virtualItems.push({
name: "current-log",
path: "virtual:current-log",
type: "file", // Для применения иконки @ по умолчанию
type: "file",
icon: "file-text"
});
virtualItems.push({
@ -70,10 +72,18 @@ export class AutocompleteManager {
await this.updateCacheIfNeeded();
const allFiles = this.fileCache.get('all') || [];
// Объединяем: виртуальные вначале, потом поиск по файлам
const filteredFiles = !query.trim()
? allFiles
: allFiles.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery));
let filteredFiles = allFiles;
// Применяем строгий фильтр расширения (если передан, например для '.py' скриптов)
if (filterExtension) {
const ext = `.${filterExtension.toLowerCase()}`;
filteredFiles = filteredFiles.filter(item => item.path.toLowerCase().endsWith(ext));
}
// Фильтруем по введенному запросу пользователя
filteredFiles = !query.trim()
? filteredFiles
: filteredFiles.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery));
return [...virtualItems, ...filteredFiles].slice(0, AutocompleteManager.MAX_TOTAL_ITEMS);
}
@ -173,26 +183,28 @@ export class AutocompleteManager {
* Обновляет кеш файлов и промптов
*/
private async updateCache(): Promise<void> {
const files = this.plugin.app.vault.getMarkdownFiles();
// Получаем ВСЕ файлы хранилища, чтобы поддерживать скрипты (.py), картинки и др.
const files = this.plugin.app.vault.getFiles();
const fileItems: AutocompleteItem[] = [];
const promptItems: AutocompleteItem[] = [];
// Нормализуем пути исключенных папок
const normalizedExcludedFolders = this.plugin.settings.excludedFolders
.filter((folder) => folder.trim() !== '')
.map((folder) => (folder.endsWith('/') ? folder : folder + '/')) // Убедимся, что заканчивается на слэш
.map((folder) => (folder.startsWith('/') ? folder.substring(1) : folder)); // Удаляем начальный слэш
.map((folder) => (folder.endsWith('/') ? folder : folder + '/'))
.map((folder) => (folder.startsWith('/') ? folder.substring(1) : folder));
for (const file of files) {
// Пропускаем файлы из исключенных папок
if (this.isFileInExcludedFolder(file, normalizedExcludedFolders)) {
continue;
}
const isPrompt = this.isPromptFile(file);
const item: AutocompleteItem = {
name: file.basename,
// Для промптов оставляем basename (без расширения),
// а для обычных файлов используем name (с расширением) для точной идентификации
name: isPrompt ? file.basename : file.name,
path: file.path,
type: this.isPromptFile(file) ? 'prompt' : 'file',
type: isPrompt ? 'prompt' : 'file',
icon: this.getFileIcon(file),
file: file
};
@ -204,7 +216,6 @@ export class AutocompleteManager {
}
}
// Сортируем по имени
fileItems.sort((a, b) => a.name.localeCompare(b.name));
promptItems.sort((a, b) => a.name.localeCompare(b.name));
@ -238,6 +249,7 @@ export class AutocompleteManager {
switch (extension) {
case 'md':
return 'file-text';
case 'py': return 'file-terminal';
case 'js':
case 'ts':
return 'code';

View File

@ -44,8 +44,9 @@ export class ReferenceAutocomplete {
* Показывает автокомплит с опциями.
* @param reference - объект ReferenceMatch, определяющий текущий вводимый референс.
* @param cursorRect - DOMRect объекта курсора, для определения позиции.
* @param filterExtension - Опциональный фильтр расширений
*/
async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise<void> {
async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect, filterExtension?: string): Promise<void> {
this.currentReference = reference;
// Для внешних файлов ($, !$) сразу открываем диалог
@ -57,10 +58,10 @@ export class ReferenceAutocomplete {
return;
}
// Для файлов и промптов работаем как раньше
let options: AutocompleteItem[] = [];
if (reference.type === 'file') {
options = await this.autocompleteManager.getFileOptions(reference.name);
// ИЗМЕНЕНО: Передаем фильтр в менеджер
options = await this.autocompleteManager.getFileOptions(reference.name, filterExtension);
} else if (reference.type === 'prompt') {
options = await this.autocompleteManager.getPromptOptions(reference.name);
} else if (reference.type === 'url') {

View File

@ -158,7 +158,7 @@ export class ChatView extends ItemView {
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number): Promise<void> {
async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number, agencyMode: boolean = false): Promise<void> {
try {
// ЗАЩИТА: Если граф не выбран, создаем его на лету, чтобы не потерять текст
if (!this.selectedGraphId) {
@ -213,7 +213,7 @@ export class ChatView extends ItemView {
const systemPromptToUse =
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature);
await this.streamLLMResponse(this.selectedGraphId!, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature, agencyMode);
} else {
throw new Error('Graph ID не определён после отправки сообщения');
}
@ -226,10 +226,15 @@ export class ChatView extends ItemView {
handleSettingsChanged(): void {
if (this.chatPanel)
{
// 1. Обновляем модель (это у вас уже было)
// 1. Обновляем модель
this.chatPanel.setSelectedModel(this.plugin.settings.defaultModel);
// 2. Уведомляем панель об обновлении всего объекта плагина (и настроек внутри)
// 2. Обновляем состояние тумблера из настроек плагина
if (this.chatPanel.agencyToggle) {
this.chatPanel.agencyToggle.checked = !!this.plugin.settings.agencyModeEnabled;
}
// 3. Уведомляем панель об обновлении всего объекта плагина (и настроек внутри)
this.chatPanel.updateSettings(this.plugin);
}
}
@ -294,7 +299,8 @@ export class ChatView extends ItemView {
systemPrompt: string | null = null,
cacheFolder?: string,
customMaxTokens?: number,
customTemperature?: number
customTemperature?: number,
agencyMode: boolean = false
): Promise<void> {
// Разворачиваем системный промпт
let finalSystemPrompt = systemPrompt;
@ -314,7 +320,8 @@ export class ChatView extends ItemView {
model: selectedModel || this.plugin.settings.defaultModel,
cache_folder: cacheFolder,
temperature: temperature,
max_tokens: customMaxTokens
max_tokens: customMaxTokens,
agency_mode: agencyMode
};
if (existingAssistantNodeId) {
@ -376,6 +383,11 @@ export class ChatView extends ItemView {
graphId: graphId,
currentNode: assistantNodeId
});
}
else if (data.type === 'tool_start' && assistantNodeId) {
if (this.chatPanel) this.chatPanel.toolExecutionUpdate(assistantNodeId, data.name, 'start', data.step);
} else if (data.type === 'tool_end' && assistantNodeId) {
if (this.chatPanel) this.chatPanel.toolExecutionUpdate(assistantNodeId, data.name, 'end', data.step);
} else if (data.type === 'chunk' && assistantNodeId) {
accumulatedContent += data.content;
this.updateStreamingMessage(assistantNodeId, accumulatedContent);
@ -496,7 +508,7 @@ export class ChatView extends ItemView {
}
}
async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string, maxTokens?: number, temperature?: number }): Promise<void> {
async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string, maxTokens?: number, temperature?: number, agencyMode?: boolean }): Promise<void> {
try {
const systemPromptToUse =
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
@ -533,7 +545,8 @@ export class ChatView extends ItemView {
const temperatureFromBackend = result.temperature;
if (data.graphId) {
await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend, cacheFolder, maxTokensFromBackend, temperatureFromBackend);
await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend,
cacheFolder, maxTokensFromBackend, temperatureFromBackend, data.agencyMode || false);
}
} catch (error: any) {
console.error('Ошибка регенерации:', error);