llm-agent-plugin/main.ts

491 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf } from 'obsidian';
import { EventBus } from './src/utils/EventBus';
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
import { WebSocketService } from './src/utils/WebSocketService';
import 'src/css/styles.css';
// ----------------------------------------------- Settings ---------------------------------------------------------------
interface LLMAgentSettings {
apiBaseUrl: string;
systemPrompt: string;
defaultModel: string;
promptsFolder: string;
enableFileReferences: boolean;
enablePromptReferences: boolean;
excludedFolders: string[];
refCacheFolder: string;
summarizationModel: string;
voiceCommandModel: string;
voiceInterval: number;
voiceDisplayLimit: number; // 3000
voiceContextLimit: number; // N2 символов для LLM
promptRegular: string;
promptCommands: string;
markerStart: string; // "старт"
markerStop: string; // "стоп"
}
const DEFAULT_SETTINGS: LLMAgentSettings = {
apiBaseUrl: 'http://localhost:5000/api',
systemPrompt: 'Ты полезный ИИ ассистент.',
defaultModel: 'gemini-2.5-flash-r',
promptsFolder: 'prompts', // Папка с промптами по умолчанию
enableFileReferences: true,
enablePromptReferences: true,
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], // Папка "templates" по умолчанию исключена
refCacheFolder: 'ref-cache', // По умолчанию относительно vault
voiceModel: '',
voiceInterval: 1,
voiceDisplayLimit: 3000,
voiceContextLimit: 3000,
promptRegular: '',
promptCommands: '',
markerStart: 'старт',
markerStop: 'стоп'
};
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
export default class LLMAgentPlugin extends Plugin {
settings: LLMAgentSettings;
eventBus: EventBus;
webSocketService: WebSocketService;
activeStreamControllers: Map<string, AbortController>;
availableModels: string[];
async syncSettings() {
try {
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.settings)
});
if (response.ok) {
console.log("LLM Agent: Settings synced with backend.");
}
} catch (e) {
console.error("LLM Agent: Failed to sync settings", e);
}
}
/**
* Загружает список доступных моделей с бэкенда.
* @returns {Promise<void>}
*/
async fetchAvailableModels(): Promise<void> {
try {
const response = await fetch(`${this.settings.apiBaseUrl}/models`);
if (!response.ok) {
throw new Error(`Ошибка загрузки моделей: ${response.status}`);
}
this.availableModels = await response.json();
} catch (error) {
console.error('Ошибка загрузки доступных моделей:', error);
// Fallback к базовым моделям
this.availableModels = ['gemini-2.5-flash', 'gpt-4o-mini', 'claude-3-5-sonnet-20241022'];
}
}
async onload() {
await this.loadSettings();
this.eventBus = new EventBus();
this.webSocketService = new WebSocketService(this.eventBus, this.settings.apiBaseUrl);
this.webSocketService.connect();
// Регистрируем кастомные views
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
// Добавляем команды
this.addCommand({
id: 'open-llm-agent-graph',
name: 'Открыть граф диалогов LLM Agent',
callback: () => this.activateGraphView()
});
this.addCommand({
id: 'open-llm-agent-chat',
name: 'Открыть чат LLM Agent',
callback: () => this.activateChatView()
});
this.addCommand({
id: 'new-llm-agent-chat',
name: 'Новый чат LLM Agent',
callback: () => this.handleNewChat()
});
// Добавляем иконку в ribbon
this.addRibbonIcon('brain-circuit', 'LLM Agent', () => {
this.activateGraphView();
this.activateChatView();
});
console.log('LLM Agent plugin загружен');
this.activeStreamControllers = new Map<string, AbortController>();
this.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
this.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
await this.fetchAvailableModels();
// Вызываем один раз при запуске
await this.syncSettings();
}
onunload() {
if (this.webSocketService) {
this.webSocketService.disconnect();
}
this.activeStreamControllers.clear();
this.eventBus.off('stream-started', this.handleStreamStarted);
this.eventBus.off('stream-ended', this.handleStreamEnded);
console.log('LLM Agent plugin выгружен');
}
// ----------------------------------------------- Settings Management ---------------------------------------------------------------
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
this.eventBus.emit('settings-changed');
}
// ----------------------------------------------- View Management ---------------------------------------------------------------
async activateGraphView() {
const existingLeaf = this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE)[0];
if (existingLeaf) {
this.app.workspace.revealLeaf(existingLeaf);
} else {
await this.app.workspace.getLeaf('tab').setViewState({
type: GRAPH_VIEW_TYPE,
active: true
});
}
}
async activateChatView() {
const existingLeaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0];
if (existingLeaf) {
this.app.workspace.revealLeaf(existingLeaf);
} else {
const rightLeaf = this.app.workspace.getRightLeaf(false);
if (rightLeaf) {
await rightLeaf.setViewState({
type: CHAT_VIEW_TYPE,
active: true
});
}
}
}
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
handleNewChat() {
this.eventBus.emit('new-chat');
}
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
this.activeStreamControllers.set(data.nodeId, data.controller);
}
handleStreamEnded(data: { nodeId: string }): void {
this.activeStreamControllers.delete(data.nodeId);
}
}
// ----------------------------------------------- Sample Setting Tab ---------------------------------------------------------------
class SampleSettingTab extends PluginSettingTab {
plugin: LLMAgentPlugin;
constructor(app: App, plugin: LLMAgentPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Основные настройки' });
new Setting(containerEl)
.setName('Модель по умолчанию')
.setDesc('Выберите модель для основного чата.')
.addDropdown((dropdown) => {
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
dropdown.setValue(this.plugin.settings.defaultModel)
.onChange(async (value) => {
this.plugin.settings.defaultModel = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
});
});
new Setting(containerEl)
.setName('Модель суммаризации')
.setDesc('Выберите модель для суммаризации.')
.addDropdown((dropdown) => {
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
dropdown.setValue(this.plugin.settings.summarizationModel)
.onChange(async (value) => {
this.plugin.settings.summarizationModel = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
});
});
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();
})
);
new Setting(containerEl)
.setName('Системный промпт')
.setDesc('Этот промпт будет отправляться LLM перед каждым диалогом.')
.addTextArea((text) =>
text
.setPlaceholder('Ты полезный ИИ ассистент.')
.setValue(this.plugin.settings.systemPrompt)
.onChange(async (value) => {
this.plugin.settings.systemPrompt = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
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();
});
});
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('Сколько последних символов транскрибации отправлять в модель.')
.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('Регулярный промпт')
.setDesc('Инструкции для Промпта №1.')
.addTextArea((text) =>
text
// .setPlaceholder('prompts/regular_analysis.md')
.setValue(this.plugin.settings.promptRegular)
.onChange(async (value) => {
this.plugin.settings.promptRegular = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Промпт команд')
.setDesc('Инструкции для Промпта №2.')
.addTextArea((text) =>
text
// .setPlaceholder('prompts/voice_commands.md')
.setValue(this.plugin.settings.promptCommands)
.onChange(async (value) => {
this.plugin.settings.promptCommands = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
new Setting(containerEl)
.setName('Папка с промптами')
.setDesc('Путь к папке, содержащей промпты для команд #/##')
.addText((text) =>
text
.setPlaceholder('prompts')
.setValue(this.plugin.settings.promptsFolder)
.onChange(async (value) => {
this.plugin.settings.promptsFolder = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
new Setting(containerEl)
.setName('Папка кеша для вложений')
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
.addText((text) =>
text
.setPlaceholder('ref-cache')
.setValue(this.plugin.settings.refCacheFolder)
.onChange(async (value) => {
this.plugin.settings.refCacheFolder = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
// Настройки ссылок на файлы
new Setting(containerEl)
.setName('Включить ссылки на файлы')
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
this.plugin.settings.enableFileReferences = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
// Настройки ссылок на промпты
new Setting(containerEl)
.setName('Включить ссылки на промпты')
.setDesc('Разрешить использование #/## ссылок на промпты')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enablePromptReferences).onChange(async (value) => {
this.plugin.settings.enablePromptReferences = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
// Новое поле для настройки исключенных папок
new Setting(containerEl)
.setName('Исключенные папки для ссылок')
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
this.plugin.settings.enableFileReferences = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
})
);
// Динамический список исключенных папок
containerEl.createEl('h3', { text: 'Исключенные папки' });
this.plugin.settings.excludedFolders.forEach((folder, index) => {
new Setting(containerEl)
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
.addText((text) => {
text.setPlaceholder('Путь к папке (например, templates/)' + index)
.setValue(folder)
.onChange(async (value) => {
this.plugin.settings.excludedFolders[index] = value;
await this.plugin.saveSettings();
await this.plugin.syncSettings();
// Очищаем кэш автокомплита при изменении настроек
this.plugin.eventBus.emit('settings-changed');
});
})
.addExtraButton((button) => {
button
.setIcon('x')
.setTooltip('Удалить папку')
.onClick(async () => {
this.plugin.settings.excludedFolders.splice(index, 1);
await this.plugin.saveSettings();
await this.plugin.syncSettings();
this.display(); // Перерисовать настройки для обновления списка
// Очищаем кэш автокомплита при изменении настроек
this.plugin.eventBus.emit('settings-changed');
});
});
});
new Setting(containerEl).addButton((button) => {
button
.setButtonText('Добавить папку')
.setIcon('plus')
.onClick(async () => {
this.plugin.settings.excludedFolders.push(''); // Добавляем пустое поле
await this.plugin.saveSettings();
await this.plugin.syncSettings();
this.display(); // Перерисовать настройки для обновления списка
});
});
}
}