diff --git a/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts b/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts index 5ab537d..17e51f8 100644 --- a/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts +++ b/AIClasses/FunctionDefinitions/AIFunctionDefinitions.ts @@ -1,17 +1,18 @@ import type { IAIFunctionDefinition } from "./IAIFunctionDefinition"; import { SearchVaultFiles } from "./Functions/SearchVaultFiles"; -import { ReadFile } from "./Functions/ReadFile"; +import { ReadVaultFile } from "./Functions/ReadVaultFile"; +import { WriteVaultFile } from "./Functions/WriteVaultFile"; export class AIFunctionDefinitions { public getQueryActions(destructive: boolean): IAIFunctionDefinition[] { - const actions = [ + let actions = [ SearchVaultFiles, - ReadFile + ReadVaultFile ]; if (destructive) { - actions.concat([ - + actions = actions.concat([ + WriteVaultFile ]); } diff --git a/AIClasses/FunctionDefinitions/Functions/ReadFile.ts b/AIClasses/FunctionDefinitions/Functions/ReadFile.ts deleted file mode 100644 index bbb5af0..0000000 --- a/AIClasses/FunctionDefinitions/Functions/ReadFile.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AIFunction } from "Enums/AIFunction"; -import type { IAIFunctionDefinition } from "../IAIFunctionDefinition"; - -export const ReadFile: IAIFunctionDefinition = { - name: AIFunction.ReadFile, - description: `Reads and returns the complete content of a specific file from the vault. - Call this when you need to access existing note content to answer questions, - provide summaries, verify information, or gather context before making updates. - Use proactively before updating files to understand current content and avoid - data loss. Essential for any operation that references or builds upon existing notes.`, - parameters: { - type: "object", - properties: { - file_path: { - type: "string", - description: "The full path to the file within the vault (e.g., 'folder/note.md')" - }, - user_message: { - type: "string", - description: "A short message explaining why you're reading this file (e.g., 'Reading your daily note to check tasks')" - } - }, - required: ["file_path", "user_message"] - } - } \ No newline at end of file diff --git a/AIClasses/FunctionDefinitions/Functions/ReadVaultFile.ts b/AIClasses/FunctionDefinitions/Functions/ReadVaultFile.ts new file mode 100644 index 0000000..8793d53 --- /dev/null +++ b/AIClasses/FunctionDefinitions/Functions/ReadVaultFile.ts @@ -0,0 +1,25 @@ +import { AIFunction } from "Enums/AIFunction"; +import type { IAIFunctionDefinition } from "../IAIFunctionDefinition"; + +export const ReadVaultFile: IAIFunctionDefinition = { + name: AIFunction.ReadVaultFile, + description: `Reads and returns the complete content of a specific file from the vault. + Call this when you need to access existing note content to answer questions, + provide summaries, verify information, or gather context before making updates. + Use proactively before updating files to understand current content and avoid + data loss. Essential for any operation that references or builds upon existing notes.`, + parameters: { + type: "object", + properties: { + file_path: { + type: "string", + description: "The full path to the file within the vault (e.g., 'folder/note.md')" + }, + user_message: { + type: "string", + description: "A short message to be displayed to the user explaining why you're reading this file (e.g., 'Reading your daily note to check tasks')" + } + }, + required: ["file_path", "user_message"] + } +} \ No newline at end of file diff --git a/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts b/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts index a0fe266..6517c5b 100644 --- a/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts +++ b/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts @@ -3,12 +3,17 @@ import type { IAIFunctionDefinition } from "../IAIFunctionDefinition"; export const SearchVaultFiles: IAIFunctionDefinition = { name: AIFunction.SearchVaultFiles, - description: `Searches through all files in the user's Obsidian vault for the given search term. - Uses regex pattern matching to search file content. - Returns matching vault files with metadata (names, paths) and contextual snippets showing matched content with surrounding text to enable relevance assessment. - Call this whenever you need to know what files exist in the vault to answer questions, - verify file presence, or to perform further agentic functions. - Use proactively when vault contents would inform your response.`, + description: `Searches the content of all vault files using regex pattern matching. + Returns files containing the search term with contextual snippets showing where matches appear. + + **IMPORTANT: When a search returns 0 results, a complete list of all vault files will be automatically returned.** + This allows you to verify the search scope and attempt alternative search strategies. + + Use this function when you need to: + - Find files based on what's written INSIDE them + - Search for specific concepts, keywords, or text within notes + - Locate content that matches a pattern or phrase + - Answer questions about what the user has written about a topic`, parameters: { type: "object", properties: { diff --git a/AIClasses/FunctionDefinitions/Functions/WriteVaultFile.ts b/AIClasses/FunctionDefinitions/Functions/WriteVaultFile.ts new file mode 100644 index 0000000..d9cefe3 --- /dev/null +++ b/AIClasses/FunctionDefinitions/Functions/WriteVaultFile.ts @@ -0,0 +1,28 @@ +import { AIFunction } from "Enums/AIFunction"; +import type { IAIFunctionDefinition } from "../IAIFunctionDefinition"; + +export const WriteVaultFile: IAIFunctionDefinition = { + name: AIFunction.WriteVaultFile, + description: `Writes content to a file, creating it if it doesn't exist or replacing its contents if it does. + Use this for creating new notes or completely updating existing ones when you have the full content ready. + IMPORTANT: This replaces the entire file content - always read the file first with ${AIFunction.ReadVaultFile} if you need to preserve existing content and make partial changes. + For simple updates or additions, reading first ensures you don't lose data.`, + parameters: { + type: "object", + properties: { + file_path: { + type: "string", + description: "The full path to the file within the vault (e.g., 'folder/note.md')" + }, + content: { + type: "string", + description: "The complete content to write to the file. This will replace any existing content." + }, + user_message: { + type: "string", + description: "A short message to be displayed to the user explaining what you're writing and why (e.g., 'Creating your daily note for today' or 'Updating project plan with new tasks')" + } + }, + required: ["file_path", "content", "user_message"] + } +} \ No newline at end of file diff --git a/AIClasses/SystemPrompt.ts b/AIClasses/SystemPrompt.ts index aa5ea2e..2e66209 100644 --- a/AIClasses/SystemPrompt.ts +++ b/AIClasses/SystemPrompt.ts @@ -5,7 +5,12 @@ You are a specialized AI assistant with direct access to the user's Obsidian vau ## Critical Operating Principles -### 1. Wiki-Link Everything from the Vault +### 1. Request Completion +- Execute ALL necessary operations before concluding your turn +- Ensure the user's complete request is fulfilled, not just the first step +- For multi-step tasks, gather all information before presenting findings + +### 2. Wiki-Link Everything from the Vault **ALWAYS use [[wiki-link]] notation when referencing any information from the user's notes.** - Every mention of a note, concept, person, or topic from the vault must be linked - This builds the knowledge graph and helps users navigate their information @@ -18,12 +23,7 @@ Examples: - ✅ "This relates to your ideas about [[Machine Learning]] in [[Research Notes]]" - ❌ "Based on your Project Alpha notes, the deadline is next month" (missing links) -### 2. Request Completion -- Execute ALL necessary operations before concluding your turn -- Ensure the user's complete request is fulfilled, not just the first step -- For multi-step tasks, gather all information before presenting findings - -### 2. Communication Efficiency +### 3. Communication Efficiency When performing research or multi-step operations: - Execute operations to completion - Present a single, comprehensive response with findings @@ -31,7 +31,7 @@ When performing research or multi-step operations: - Focus on RESULTS, not process narration - Only mention your methodology when it adds essential context -### 3. Vault-First Decision Framework +### 4. Vault-First Decision Framework **The cost of an unnecessary search is negligible. Missing relevant user information is costly.** @@ -52,7 +52,7 @@ When performing research or multi-step operations: Acknowledge the search, then provide general assistance: "I searched your vault but didn't find notes about [topic]. Here's what I can tell you: [general information]. Would you like me to create a note about this?" -### 4. Semantic Directory Architecture +### 5. Semantic Directory Architecture **Directory names are semantic filters, not just organizational containers.** @@ -74,7 +74,7 @@ When a query contains qualifiers that match directory names, those directories d #### Anti-Pattern: Showing all files with keyword "template" when user asked for "important templates" and '/Important templates/' directory exists. -### 5. Progressive Search Strategy +### 6. Progressive Search Strategy **NEVER accept a failed search as final. Always try multiple approaches before concluding information doesn't exist.** diff --git a/Enums/AIFunction.ts b/Enums/AIFunction.ts index 6acf770..6328ddc 100644 --- a/Enums/AIFunction.ts +++ b/Enums/AIFunction.ts @@ -1,6 +1,7 @@ export enum AIFunction { SearchVaultFiles = "search_vault_files", - ReadFile = "read_file", + ReadVaultFile = "read_vault_file", + WriteVaultFile = "write_vault_file", // used by gemini RequestWebSearch = "request_web_search" diff --git a/Services/AIFunctionService.ts b/Services/AIFunctionService.ts index c951255..64bb6d0 100644 --- a/Services/AIFunctionService.ts +++ b/Services/AIFunctionService.ts @@ -5,9 +5,10 @@ import { AIFunction } from "Enums/AIFunction"; import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse"; import type { AIFunctionCall } from "AIClasses/AIFunctionCall"; import type { SearchMatch } from "../Helpers/SearchTypes"; +import { normalizePath, TFile } from "obsidian"; export class AIFunctionService { - + private fileSystemService: FileSystemService = Resolve(Services.FileSystemService); public async performAIFunction(functionCall: AIFunctionCall): Promise { @@ -15,8 +16,11 @@ export class AIFunctionService { case AIFunction.SearchVaultFiles: return new AIFunctionResponse(functionCall.name, await this.searchVaultFiles(functionCall.arguments.search_term)); - case AIFunction.ReadFile: - return new AIFunctionResponse(functionCall.name, await this.readFile(functionCall.arguments.file_path)); + case AIFunction.ReadVaultFile: + return new AIFunctionResponse(functionCall.name, await this.readVaultFile(functionCall.arguments.file_path)); + + case AIFunction.WriteVaultFile: + return new AIFunctionResponse(functionCall.name, await this.writeVaultFile(functionCall.arguments.file_path, functionCall.arguments.content)); // this is only used by gemini case AIFunction.RequestWebSearch: @@ -34,6 +38,15 @@ export class AIFunctionService { private async searchVaultFiles(searchTerm: string): Promise { const matches: SearchMatch[] = await this.fileSystemService.searchVaultFiles(searchTerm); + + if (matches.length === 0) { + const files: TFile[] = await this.fileSystemService.listFilesInDirectory("/"); + return files.map((file) => ({ + name: file.basename, + path: file.path + })); + } + return matches.map((match) => ({ name: match.file.basename, path: match.file.path, @@ -44,11 +57,16 @@ export class AIFunctionService { })); } - private async readFile(filePath: string): Promise { + private async readVaultFile(filePath: string): Promise { const content = await this.fileSystemService.readFile(filePath); if (content === null) { return { error: `File not found: ${filePath}` }; } return { content }; } + + private async writeVaultFile(filePath: string, content: string): Promise { + const result: boolean = await this.fileSystemService.writeFile(normalizePath(filePath), content); + return isBoolean(result) ? { success: result } : { success: false, error: result } + } } \ No newline at end of file diff --git a/Services/FileSystemService.ts b/Services/FileSystemService.ts index 7d4e0ab..bc5d0e9 100644 --- a/Services/FileSystemService.ts +++ b/Services/FileSystemService.ts @@ -30,7 +30,7 @@ export class FileSystemService { return null; } - public async writeFile(filePath: string, content: string, allowAccessToPluginRoot: boolean = false): Promise { + public async writeFile(filePath: string, content: string, allowAccessToPluginRoot: boolean = false): Promise { try { let file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot); if (file == null || !(file instanceof TFile)) { @@ -43,7 +43,7 @@ export class FileSystemService { } catch (error) { console.error("Error writing file:", error); - return false; + return error; } }