diff --git a/src/agent-modes.ts b/src/agent-modes.ts index b2be302..a9b7f84 100644 --- a/src/agent-modes.ts +++ b/src/agent-modes.ts @@ -25,10 +25,7 @@ function filterToolsByName(tools: OllamaTool[], allowed: Set): OllamaToo return tools.filter((t) => allowed.has(t.function.name)); } -const READ_TOOLS = new Set([ - 'read_vault_file', - 'search_vault_files', -]); +const READ_TOOLS = new Set(['read_vault_file', 'search_vault_files']); const ORGANIZE_TOOLS = new Set([ 'read_vault_file', @@ -52,10 +49,7 @@ const EDIT_TOOLS = new Set([ 'insert_link', ]); -const RESEARCH_TOOLS = new Set([ - 'read_vault_file', - 'search_vault_files', -]); +const RESEARCH_TOOLS = new Set(['read_vault_file', 'search_vault_files']); export const AGENT_MODE_CONFIGS: Record = { ask: { @@ -63,6 +57,8 @@ export const AGENT_MODE_CONFIGS: Record = { description: 'Answer questions using vault context. Read-only mode.', systemPrompt: `You are a helpful assistant that answers questions using the contents of the user's Obsidian vault. You have access to search and read tools to find relevant information. +IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to search_vault_files or read_vault_file. +Only respond to the user after you have received and analyzed the tool results. Always base your answers on vault content when possible. If you cannot find relevant information, say so clearly. Do not make up facts.`, @@ -76,6 +72,8 @@ Do not make up facts.`, description: 'Create, modify, and organize notes with full editing tools.', systemPrompt: `You are an assistant that helps edit and manage notes in the user's Obsidian vault. You have full access to reading, searching, creating, appending, renaming, moving, and deleting notes. +IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to the appropriate tool. +Only respond to the user after you have received and analyzed the tool results. When editing notes: - Prefer modifying existing content over creating duplicates. - Use the replace_note_section tool to update specific sections. @@ -92,6 +90,8 @@ When editing notes: description: 'Tag, rename, move, and link notes to keep the vault tidy.', systemPrompt: `You are an assistant that helps organize the user's Obsidian vault. You can search notes, read them, update frontmatter tags, rename files, move files to folders, and insert wiki-links. +IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to the appropriate tool. +Only respond to the user after you have received and analyzed the tool results. When organizing: - Suggest consistent tag vocabularies. - Group related notes by linking them. @@ -107,6 +107,8 @@ When organizing: description: 'Deep vault search and synthesis across multiple notes.', systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault. Your job is to synthesize information across multiple notes, find connections, and produce comprehensive summaries. +IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to search_vault_files or read_vault_file. +Only respond to the user after you have received and analyzed the tool results. Search broadly, read key sources, and cross-reference information. Cite specific notes and quotes where possible. If information is incomplete or contradictory, note it explicitly.`, diff --git a/src/chat-view.ts b/src/chat-view.ts index 67c04b1..c3b13c1 100755 --- a/src/chat-view.ts +++ b/src/chat-view.ts @@ -76,7 +76,9 @@ export class ChatView extends ItemView { this.toolExecutor = new ToolExecutor(this.app.vault, this.app, telemetryManager); this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app); this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer); - this.conversationStateManager = new ConversationStateManager(); + this.conversationStateManager = new ConversationStateManager( + getSystemPromptForMode(this.currentAgentMode) + ); this.structuredMemoryManager = structuredMemoryManager; this.telemetryManager = telemetryManager; this.workflowEngine = new WorkflowEngine( @@ -365,11 +367,12 @@ export class ChatView extends ItemView { if (this.modeSelectorEl) { this.modeSelectorEl.value = mode; } + this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode)); } clearConversation(): void { this.messages = []; - this.conversationStateManager.clear(); + this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode)); this.render(); } @@ -623,43 +626,6 @@ export class ChatView extends ItemView { return baseMessages; } - buildMessages(userMessageContent: string, tools?: OllamaTool[]): OllamaMessage[] { - const systemContent = getSystemPromptForMode(this.currentAgentMode); - const messages: OllamaMessage[] = []; - - // Inject structured memory as a preceding system message if available - if (this.structuredMemoryManager) { - const memoryContext = this.structuredMemoryManager.buildMemoryContext(); - if (memoryContext) { - messages.push({ - role: 'system', - content: memoryContext, - }); - } - } - - messages.push({ - role: 'system', - content: systemContent, - }); - - const userMessage: OllamaMessage = { - role: 'user', - content: userMessageContent, - }; - - messages.push(userMessage); - - if (tools && tools.length > 0) { - messages.push({ - role: 'assistant', - content: 'I have access to the following tools to help answer your questions:', - }); - } - - return messages; - } - async processToolCalls( toolCalls: OllamaToolCall[], messages: OllamaMessage[], diff --git a/src/conversation-state.ts b/src/conversation-state.ts index ba8ef54..4787d3d 100644 --- a/src/conversation-state.ts +++ b/src/conversation-state.ts @@ -8,6 +8,10 @@ export interface ConversationState { longTermContext: OllamaMessage[]; } +const DEFAULT_SYSTEM_PROMPT = `You are an assistant that can help answer questions using the contents of a vault. +When a user asks for information about their vault, you MUST call the search_vault_files or read_vault_file tool to find the answer. +Do not say you will search or read files — immediately emit the tool_call.`; + export class ConversationStateManager { private shortTermContext: OllamaMessage[] = []; private mediumTermContext: OllamaMessage[] = []; @@ -15,15 +19,12 @@ export class ConversationStateManager { private maxShortTermTurns: number = 10; private maxMediumTermMessages: number = 20; - constructor() { + constructor(initialSystemPrompt?: string) { // Initialize with default system context this.longTermContext = [ { role: 'system', - content: `You are an assistant that can help answer questions using the contents of a vault. - The user can ask questions about their vault contents, and you should provide helpful responses based on the files. - When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool. - Only use the tools if you need to access vault content that is not already in the context.`, + content: initialSystemPrompt ?? DEFAULT_SYSTEM_PROMPT, }, ]; } @@ -60,17 +61,18 @@ export class ConversationStateManager { * Sets the user's persona or core knowledge as long-term context * @param personaContent The persona or core knowledge content */ - setPersona(personaContent: string): void { - // Remove any existing persona messages - this.longTermContext = this.longTermContext.filter( - (msg) => - msg.role !== 'system' || - !msg.content.includes( - 'You are an assistant that can help answer questions using the contents of a vault' - ) - ); + setSystemPrompt(systemPrompt: string): void { + // Replace all existing system messages with the new system prompt + this.longTermContext = this.longTermContext.filter((msg) => msg.role !== 'system'); + this.longTermContext.unshift({ + role: 'system', + content: systemPrompt, + }); + } - // Add the new persona + setPersona(personaContent: string): void { + // Replace all existing system messages with the new persona + this.longTermContext = this.longTermContext.filter((msg) => msg.role !== 'system'); this.longTermContext.push({ role: 'system', content: personaContent, @@ -117,16 +119,13 @@ export class ConversationStateManager { /** * Clears all conversation context */ - clear(): void { + clear(systemPrompt?: string): void { this.shortTermContext = []; this.mediumTermContext = []; this.longTermContext = [ { role: 'system', - content: `You are an assistant that can help answer questions using the contents of a vault. - The user can ask questions about their vault contents, and you should provide helpful responses based on the files. - When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool. - Only use the tools if you need to access vault content that is not already in the context.`, + content: systemPrompt ?? DEFAULT_SYSTEM_PROMPT, }, ]; } diff --git a/tests/conversation-state.test.ts b/tests/conversation-state.test.ts index 4bb384d..a18aa4e 100644 --- a/tests/conversation-state.test.ts +++ b/tests/conversation-state.test.ts @@ -5,17 +5,18 @@ describe('ConversationStateManager', () => { let manager: ConversationStateManager; beforeEach(() => { - manager = new ConversationStateManager(); + manager = new ConversationStateManager( + 'You are an assistant that can help answer questions using the contents of a vault' + ); }); describe('constructor', () => { it('should initialize with default system message in long-term context', () => { - const longTerm = manager.getLongTermContext(); + const customManager = new ConversationStateManager(); + const longTerm = customManager.getLongTermContext(); expect(longTerm).toHaveLength(1); expect(longTerm[0].role).toBe('system'); - expect(longTerm[0].content).toContain( - 'You are an assistant that can help answer questions using the contents of a vault' - ); + expect(longTerm[0].content).toContain('immediately emit the tool_call'); }); it('should initialize with empty short-term and medium-term contexts', () => { @@ -64,25 +65,16 @@ describe('ConversationStateManager', () => { it('should replace default system message with custom persona', () => { manager.setPersona('You are a coding expert.'); const longTerm = manager.getLongTermContext(); - expect(longTerm.some((msg) => msg.content === 'You are a coding expert.')).toBe(true); - expect( - longTerm.some((msg) => - msg.content.includes( - 'You are an assistant that can help answer questions using the contents of a vault' - ) - ) - ).toBe(false); + expect(longTerm).toHaveLength(1); + expect(longTerm[0].content).toBe('You are a coding expert.'); }); it('should allow multiple persona updates', () => { manager.setPersona('First persona.'); manager.setPersona('Second persona.'); const longTerm = manager.getLongTermContext(); - expect(longTerm.some((msg) => msg.content === 'Second persona.')).toBe(true); - // The actual implementation filters out the default system message but keeps previous persona messages - // So we should expect to find both personas in the long-term context - expect(longTerm.some((msg) => msg.content === 'First persona.')).toBe(true); - expect(longTerm).toHaveLength(2); + expect(longTerm).toHaveLength(1); + expect(longTerm[0].content).toBe('Second persona.'); }); }); @@ -108,7 +100,7 @@ describe('ConversationStateManager', () => { // Long-term comes first expect(messages[0].role).toBe('system'); - expect(messages[0].content).toContain('You are an assistant'); + expect(messages[0].content).toContain('contents of a vault'); // Medium-term follows expect(messages[1].content).toBe('Medium'); @@ -136,9 +128,14 @@ describe('ConversationStateManager', () => { manager.setPersona('Custom persona'); manager.clear(); const longTerm = manager.getLongTermContext(); - expect(longTerm[0].content).toContain( - 'You are an assistant that can help answer questions using the contents of a vault' - ); + expect(longTerm[0].content).toContain('immediately emit the tool_call'); + }); + + it('should accept a custom system prompt when clearing', () => { + manager.setPersona('Custom persona'); + manager.clear('Custom system prompt'); + const longTerm = manager.getLongTermContext(); + expect(longTerm[0].content).toBe('Custom system prompt'); }); }); @@ -209,6 +206,7 @@ describe('ConversationStateManager', () => { const longTerm = manager.getLongTermContext(); expect(longTerm).toHaveLength(1); expect(longTerm[0].role).toBe('system'); + expect(longTerm[0].content).toContain('immediately emit the tool_call'); }); }); });