diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 773a61c0..4a65b810 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -21,7 +21,12 @@ import ChainManager from "@/LLMProviders/chainManager"; import CopilotPlugin from "@/main"; import { Mention } from "@/mentions/Mention"; import { useIsPlusUser } from "@/plusUtils"; -import { getSettings, updateSetting, useSettingsValue } from "@/settings/model"; +import { + getComposerOutputPrompt, + getSettings, + updateSetting, + useSettingsValue, +} from "@/settings/model"; import SharedState, { ChatMessage, useSharedState } from "@/sharedState"; import { FileParserManager } from "@/tools/FileParserManager"; import { err2String, formatDateTime } from "@/utils"; @@ -160,11 +165,18 @@ const Chat: React.FC = ({ setLoading(true); setLoadingMessage(LOADING_MESSAGES.DEFAULT); - // First, process the original user message for custom prompts + // First, add composer instruction if necessary + let processedInputMessage = inputMessage; + const composerPrompt = await getComposerOutputPrompt(); + if (inputMessage.includes("@composer") && composerPrompt !== "") { + processedInputMessage = + inputMessage + "\n\n\n" + composerPrompt + "\n"; + } + // process the original user message for custom prompts const customPromptProcessor = CustomPromptProcessor.getInstance(app.vault); const { processedPrompt: processedUserMessage, includedFiles } = await customPromptProcessor.processCustomPrompt( - inputMessage || "", + processedInputMessage || "", "", app.workspace.getActiveFile() as TFile | undefined ); diff --git a/src/constants.ts b/src/constants.ts index a499bc8b..ede9fbf3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,7 +25,7 @@ export const DEFAULT_SYSTEM_PROMPT = `You are Obsidian Copilot, a helpful assist 12. Do NOT mention the additional context provided such as getCurrentTime and getTimeRangeMs if it's irrelevant to the user message. 13. If the user mentions "tags", it most likely means tags in Obsidian note properties.`; -export const COMPOSER_INSTRUCTIONS = `If the user explicitly requests to update or create markdown notes or canvases OR mentions "@composer", always return the new note content in a special JSON format. +export const COMPOSER_OUTPUT_INSTRUCTIONS = `Return the new note content or canvas JSON in a special JSON format. # JSON Format Provide the content in JSON format and wrap it in a code block with the following structure: diff --git a/src/contextProcessor.ts b/src/contextProcessor.ts index aedab463..ff687fb3 100644 --- a/src/contextProcessor.ts +++ b/src/contextProcessor.ts @@ -104,10 +104,10 @@ export class ContextProcessor { content = await this.processEmbeddedPDFs(content, vault, fileParserManager); } - additionalContext += `\n\nTitle: [[${note.basename}]]\nPath: ${note.path}\n\n${content}`; + additionalContext += `\n\n \n Title: [[${note.basename}]]\nPath: ${note.path}\n\n${content}\n`; } catch (error) { console.error(`Error processing file ${note.path}:`, error); - additionalContext += `\n\nTitle: [[${note.basename}]]\nPath: ${note.path}\n\n[Error: Could not process file]`; + additionalContext += `\n\n \n Title: [[${note.basename}]]\nPath: ${note.path}\n\n[Error: Could not process file]\n`; } }; @@ -116,9 +116,14 @@ export class ContextProcessor { await processNote(activeNote); } + const includedFilePaths = new Set(); // Process context notes for (const note of contextNotes) { + if (includedFilePaths.has(note.path)) { + continue; + } await processNote(note); + includedFilePaths.add(note.path); } return additionalContext; diff --git a/src/customPromptProcessor.ts b/src/customPromptProcessor.ts index d2c0a817..8420a98a 100644 --- a/src/customPromptProcessor.ts +++ b/src/customPromptProcessor.ts @@ -91,7 +91,11 @@ async function extractVariablesFromPrompt( variablesMap.set(variableName, variableResult.content); variableResult.files.forEach((file) => includedFiles.add(file)); } else if (variableName.toLowerCase() !== "activenote") { - console.warn(`No notes found for variable: ${variableName}`); + if (variableName.startsWith('"')) { + // DO NOTHING as the user probably wants to write a JSON object + } else { + console.warn(`No notes found for variable: ${variableName}`); + } } } diff --git a/src/integration_tests/composerPrompt.test.ts b/src/integration_tests/composerPrompt.test.ts index db6baf0a..3311eee0 100644 --- a/src/integration_tests/composerPrompt.test.ts +++ b/src/integration_tests/composerPrompt.test.ts @@ -1,4 +1,4 @@ -import { DEFAULT_SYSTEM_PROMPT, COMPOSER_INSTRUCTIONS } from "../constants"; +import { DEFAULT_SYSTEM_PROMPT, COMPOSER_OUTPUT_INSTRUCTIONS } from "../constants"; import * as dotenv from "dotenv"; import { jest } from "@jest/globals"; import { @@ -55,7 +55,7 @@ describe("Composer Instructions - Integration Tests", () => { temperature: 0.1, maxOutputTokens: 1000, }, - systemInstruction: `${DEFAULT_SYSTEM_PROMPT}\n\n${COMPOSER_INSTRUCTIONS}`, + systemInstruction: DEFAULT_SYSTEM_PROMPT, safetySettings: [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, @@ -87,7 +87,9 @@ describe("Composer Instructions - Integration Tests", () => { try { // Create chat session and send message const chat = model.startChat(); - const result = await chat.sendMessage(userPrompt); + const result = await chat.sendMessage( + userPrompt + "\n\n\n" + COMPOSER_OUTPUT_INSTRUCTIONS + "\n" + ); const content = result.response.text(); if (expectedBlocks == 0) { expect(content).not.toContain('"type": "composer"'); @@ -137,7 +139,10 @@ describe("Composer Instructions - Integration Tests", () => { }; // Run tests with different prompts - testComposerResponse("Composer: create a new note", "Create a new note about climate change?"); + testComposerResponse( + "Composer: create a new note", + "@composer Create a new note about climate change?" + ); testComposerResponse( "Composer: add content to a note", @@ -150,7 +155,7 @@ describe("Composer Instructions - Integration Tests", () => { testComposerResponse( "Composer: rewrite a note", - `Rewrite the note [[atom]] to be more concise. + `@composer Rewrite the note [[atom]] to be more concise. Title: [[atom]] Path: atom.md @@ -159,7 +164,7 @@ describe("Composer Instructions - Integration Tests", () => { testComposerResponse( "Composer: remove content from a note", - `Remove the second paragraph. + `@composer Remove the second paragraph. Title: [[atom]] Path: atom.md @@ -168,15 +173,19 @@ describe("Composer Instructions - Integration Tests", () => { testComposerResponse( "Composer: update multiple notes", - "Create two notes on the topic of Earth and Mars separately", + "@composer Create two notes on the topic of Earth and Mars separately", 2 ); - testComposerResponse("Composer: create a canvas", "Create a canvas about water cycle", 1); + testComposerResponse( + "Composer: create a canvas", + "@composer Create a canvas about water cycle", + 1 + ); testComposerResponse( "Composer: not triggered on summarization", - "Summarize the note [[atom]]", + "@composer Summarize the note [[atom]]", 0 ); }); diff --git a/src/settings/model.ts b/src/settings/model.ts index b61fc3b3..f342d956 100644 --- a/src/settings/model.ts +++ b/src/settings/model.ts @@ -7,7 +7,7 @@ import { type ChainType } from "@/chainFactory"; import { BUILTIN_CHAT_MODELS, BUILTIN_EMBEDDING_MODELS, - COMPOSER_INSTRUCTIONS, + COMPOSER_OUTPUT_INSTRUCTIONS, DEFAULT_OPEN_AREA, DEFAULT_SETTINGS, DEFAULT_SYSTEM_PROMPT, @@ -250,22 +250,15 @@ export function sanitizeSettings(settings: CopilotSettings): CopilotSettings { return sanitizedSettings; } -export function getComposerPrompt(): string { +export function getComposerOutputPrompt(): string { const isPlusUser = getSettings().isPlusUser; - if (isPlusUser) { - return COMPOSER_INSTRUCTIONS; - } - - return ""; + return isPlusUser ? COMPOSER_OUTPUT_INSTRUCTIONS : ""; } export function getSystemPrompt(): string { const userPrompt = getSettings().userSystemPrompt; - let basePrompt = DEFAULT_SYSTEM_PROMPT; - - // Add composer instructions for plus users - basePrompt = `${basePrompt}\n\n${getComposerPrompt()}`; + const basePrompt = DEFAULT_SYSTEM_PROMPT; if (userPrompt) { return `${basePrompt}