diff --git a/main.ts b/main.ts index 42ae434..370af0c 100644 --- a/main.ts +++ b/main.ts @@ -72,7 +72,7 @@ export default class LLMAgentPlugin extends Plugin { // Клонируем настройки, чтобы не испортить исходные пути в конфиге плагина const settingsToSync = JSON.parse(JSON.stringify(this.settings)); - // Собираем промпты (раскрываем все цепочки ссылок) + // Собираем промпты (раскрываем все цепочки ссылок) для работы с аудиологами на бекенде settingsToSync.systemPrompt = await engine.processRawSettingValue(this.settings.systemPrompt); settingsToSync.promptRegular = await engine.processRawSettingValue(this.settings.promptRegular); settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands); diff --git a/src/references/CacheManager.ts b/src/references/CacheManager.ts index 862d956..d94c4ab 100644 --- a/src/references/CacheManager.ts +++ b/src/references/CacheManager.ts @@ -7,6 +7,7 @@ import { TFile, normalizePath, FileSystemAdapter } from 'obsidian'; import LLMAgentPlugin from 'main'; import { v4 as uuidv4 } from 'uuid'; // Нужно установить: npm install uuid @types/uuid import { getMimeType } from 'src/utils/fileUtils'; +import { TemplateEngine } from './TemplateEngine'; export class CacheManager { private plugin: LLMAgentPlugin; @@ -67,11 +68,9 @@ export class CacheManager { await this.ensureCacheFolderExists(); const uuid = uuidv4(); - const extension = file.extension; + const extension = file.extension.toLowerCase(); const cachedFileName = `${file.basename}-${uuid}.${extension}`; - const content = await this.plugin.app.vault.readBinary(file); - const cacheFolderPath = this.getCacheFolderPath(); const cachedFilePath = `${cacheFolderPath}/${cachedFileName}`; @@ -80,7 +79,25 @@ export class CacheManager { } const fs = window.require('fs').promises; - await fs.writeFile(cachedFilePath, Buffer.from(content)); + + // Список расширений, внутри которых мы ищем и разворачиваем макросы + const textExtensions = ['md', 'txt']; // , 'canvas' + + if (textExtensions.includes(extension)) { + // Читаем как текст + let content = await this.plugin.app.vault.read(file); + + // Инициализируем движок и полностью разворачиваем все вложенные @{} и #{} + const templateEngine = new TemplateEngine(this.plugin); + const expandedContent = await templateEngine.expandPromptTemplate(content); + + // Сохраняем в кеш РАЗВЕРНУТЫЙ текст (снапшот) + await fs.writeFile(cachedFilePath, expandedContent, 'utf8'); + } else { + // Если это картинка, PDF или другой бинарник, просто копируем байты + const content = await this.plugin.app.vault.readBinary(file); + await fs.writeFile(cachedFilePath, Buffer.from(content)); + } return { uuid: uuid, @@ -122,6 +139,7 @@ export class CacheManager { }; } + // TODO: Кажется, не используется /** * Восстанавливает кеш из исходного файла */ diff --git a/src/references/ReferenceExpander.ts b/src/references/ReferenceExpander.ts index 77cec55..d48dded 100644 --- a/src/references/ReferenceExpander.ts +++ b/src/references/ReferenceExpander.ts @@ -26,58 +26,6 @@ export class ReferenceExpander { // ----------------------------------------------- Public Methods --------------------------------------------------------------- - /** - * Заменяет все ссылки на содержимое файлов (перед отправкой на сервер) - * @param text - текст с ссылками - * @param references - массив ссылок для обработки - * @returns текст с развернутыми ссылками - */ - /*async expandReferences(text: string, references: FileReference[]): Promise { - if (references.length === 0) { - return text; - } - - let expandedText = text; - - // Обрабатываем ссылки от конца к началу, чтобы не сбивать индексы - const sortedReferences = [...references].sort((a, b) => { - const aMatches = ReferenceParser.parseReferences(text).filter((m) => m.name === a.name); - const bMatches = ReferenceParser.parseReferences(text).filter((m) => m.name === b.name); - const aIndex = aMatches.length > 0 ? aMatches[aMatches.length - 1].startIndex : 0; - const bIndex = bMatches.length > 0 ? bMatches[bMatches.length - 1].startIndex : 0; - return bIndex - aIndex; - }); - - for (const reference of sortedReferences) { - expandedText = await this.expandSingleReference(expandedText, reference); - } - - return expandedText; - }*/ - - /** - * Обрабатывает историю сообщений, раскрывая постоянные ссылки (@@/##) - * @param messages - массив сообщений - * @returns обработанные сообщения - */ - /*async expandMessageHistory(messages: any[]): Promise { - const expandedMessages = []; - - for (const message of messages) { - if (message.role === 'user') { - const expandedContent = await this.expandPermanentReferences(message.content); - expandedMessages.push({ - ...message, - content: expandedContent - }); - } else { - expandedMessages.push(message); - } - } - - return expandedMessages; - }*/ - /** * Извлекает вложения из текста и кеширует файлы. * Обрабатывает как физические файлы, так и виртуальные (current-log). @@ -195,221 +143,4 @@ export class ReferenceExpander { attachments: attachments }; } - - /** - * Подготавливает сообщения для LLM с учетом вложений. - * Раскрывает только постоянные (@@/##) ссылки и ссылки в последнем сообщении. - */ - /*async prepareMessagesForLLM(messages: ChatHistoryItem[], isLastMessage: boolean = false): Promise { - const preparedMessages = []; - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - const isLast = i === messages.length - 1; - - if (msg.role === 'user' && msg.attachments && msg.attachments.length > 0) { - // Определяем, какие вложения нужно раскрыть - const attachmentsToExpand = msg.attachments.filter((att) => att.permanent || (isLast && isLastMessage)); - - if (attachmentsToExpand.length > 0) { - // Формируем сообщение с раскрытыми вложениями - let expandedContent = msg.content; - - for (const attachment of attachmentsToExpand) { - const formattedContent = this.formatContentForInsertion(attachment.content || '', attachment); - expandedContent += '\n' + formattedContent; - } - - preparedMessages.push({ - role: msg.role, - content: expandedContent - }); - } else { - // Вложения есть, но раскрывать не нужно - preparedMessages.push({ - role: msg.role, - content: msg.content - }); - } - } else { - // Сообщение без вложений - preparedMessages.push({ - role: msg.role, - content: msg.content - }); - } - } - - return preparedMessages; - }*/ - - // TODO: При реализации снапшотов - сохранять содержимое файлов - // async expandReferencesWithSnapshots(text: string, references: FileReference[], messageId: string): Promise<{expandedText: string, snapshots: Record}> { - // const snapshots: Record = {}; - // let expandedText = text; - // - // for (const reference of references) { - // const content = await this.loadFileContent(reference); - // if (content !== null) { - // snapshots[reference.path] = content; - // expandedText = await this.expandSingleReference(expandedText, reference, content); - // } - // } - // - // return { expandedText, snapshots }; - // } - - // ----------------------------------------------- Private Methods --------------------------------------------------------------- - - /** - * Раскрывает одну ссылку в тексте - * @param text - текст для обработки - * @param reference - ссылка для раскрытия - * @param preloadedContent - предзагруженное содержимое (для снапшотов) - * @returns текст с раскрытой ссылкой - */ - /*private async expandSingleReference(text: string, reference: FileReference, preloadedContent?: string): Promise { - const content = preloadedContent || (await this.loadFileContent(reference)); - - if (content === null) { - console.warn(`Не удалось загрузить содержимое для ссылки: ${reference.name}`); - return text; - } - - // Определяем паттерн для замены - const prefix = reference.permanent ? (reference.type === 'file' ? '@@' : '##') : reference.type === 'file' ? '@' : '#'; - - // Создаем регулярное выражение для поиска ссылки - const escapedName = this.escapeRegExp(reference.name); - const pattern = new RegExp(`${this.escapeRegExp(prefix)}(?:"${escapedName}"|${escapedName})(?=\\s|$|[^\\w-])`, 'g'); - - // Форматируем содержимое для вставки - const formattedContent = this.formatContentForInsertion(content, reference); - - return text.replace(pattern, formattedContent); - }*/ - - /** - * Раскрывает только постоянные ссылки (@@/##) в тексте - * @param text - текст для обработки - * @returns текст с раскрытыми постоянными ссылками - */ - /*private async expandPermanentReferences(text: string): Promise { - const matches = ReferenceParser.parseReferences(text); - const permanentMatches = matches.filter((match) => match.prefix.length === 2); // @@ или ## - - if (permanentMatches.length === 0) { - return text; - } - - let expandedText = text; - - // Обрабатываем ссылки от конца к началу - const sortedMatches = [...permanentMatches].sort((a, b) => b.startIndex - a.startIndex); - - for (const match of sortedMatches) { - const reference: FileReference = { - type: match.type, - name: match.name, - path: await this.resolveFilePath(match.name, match.type), - permanent: true - }; - - const content = await this.loadFileContent(reference); - if (content !== null) { - const formattedContent = this.formatContentForInsertion(content, reference); - expandedText = expandedText.substring(0, match.startIndex) + formattedContent + expandedText.substring(match.endIndex); - } - } - - return expandedText; - }*/ - - /** - * Загружает содержимое файла - * @param reference - ссылка на файл - * @returns содержимое файла или null если не найден - */ - /*private async loadFileContent(reference: FileReference): Promise { - try { - const file = this.findFile(reference.path, reference.type); - if (!file) { - return null; - } - - const content = await this.plugin.app.vault.read(file); - - // Для промптов обрабатываем шаблоны - if (reference.type === 'prompt') { - return await this.templateEngine.expandPromptTemplate(content); - } - - return content; - } catch (error) { - console.error(`Ошибка загрузки файла ${reference.path}:`, error); - return null; - } - }*/ - - /** - * Находит файл по пути - * @param path - путь к файлу - * @param type - тип файла - * @returns файл или null - */ - /*private findFile(path: string, type: 'file' | 'prompt'): TFile | null { - let file = this.plugin.app.vault.getAbstractFileByPath(path); - - if (!file && type === 'prompt') { - // Попробуем найти в папке промптов - const promptsFolder = this.plugin.settings.promptsFolder; - if (promptsFolder) { - const fullPath = `${promptsFolder}/${path}`; - file = this.plugin.app.vault.getAbstractFileByPath(fullPath); - } - } - - return file instanceof TFile ? file : null; - }*/ - - /** - * Разрешает полный путь к файлу по имени - * @param name - имя файла - * @param type - тип файла - * @returns полный путь к файлу - */ - /*private async resolveFilePath(name: string, type: 'file' | 'prompt'): Promise { - if (type === 'prompt') { - const promptsFolder = this.plugin.settings.promptsFolder; - return promptsFolder ? `${promptsFolder}/${name}.md` : `${name}.md`; - } else { - // Ищем файл в хранилище - const files = this.plugin.app.vault.getMarkdownFiles(); - const matchingFile = files.find((f) => f.basename === name); - return matchingFile ? matchingFile.path : `${name}.md`; - } - }*/ - - /** - * Форматирует содержимое для вставки в сообщение - * @param content - содержимое файла - * @param reference - информация о ссылке - * @returns отформатированное содержимое - */ - /*private formatContentForInsertion(content: string, reference: FileReference): string { - // Добавляем заголовок с информацией о файле - const header = `\n\n--- Содержимое ${reference.type === 'prompt' ? 'промпта' : 'файла'}: ${reference.name} ---\n`; - const footer = `\n--- Конец ${reference.type === 'prompt' ? 'промпта' : 'файла'}: ${reference.name} ---\n\n`; - - return header + content.trim() + footer; - }*/ - - /** - * Экранирует специальные символы для регулярного выражения - * @param string - строка для экранирования - * @returns экранированная строка - */ - /*private escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - }*/ } diff --git a/src/views/ChatView.ts b/src/views/ChatView.ts index 28e7e15..1c5371f 100644 --- a/src/views/ChatView.ts +++ b/src/views/ChatView.ts @@ -7,6 +7,7 @@ import { ItemView, Notice, WorkspaceLeaf } from 'obsidian'; import { ChatPanel } from '../components/ChatPanel'; import LLMAgentPlugin from 'main'; import { Attachment } from 'src/components/models/ChatHistoryItem'; +import { TemplateEngine } from '../references/TemplateEngine'; export const CHAT_VIEW_TYPE = 'llm-agent-chat-view'; @@ -275,10 +276,17 @@ export class ChatView extends ItemView { systemPrompt: string | null = null, cacheFolder?: string ): Promise { + // Разворачиваем системный промпт (например, если там передано "#my_system_instructions") + let finalSystemPrompt = systemPrompt; + if (systemPrompt) { + const templateEngine = new TemplateEngine(this.plugin); + finalSystemPrompt = await templateEngine.processRawSettingValue(systemPrompt); + } + const body = { graph_id: graphId, user_node_id: userNodeId, - system_prompt: systemPrompt, + system_prompt: finalSystemPrompt, model: selectedModel || this.plugin.settings.defaultModel, cache_folder: cacheFolder };