feat: rename ReadFile to ReadVaultFile and add WriteVaultFile function

- Rename ReadFile to ReadVaultFile for consistency
- Add WriteVaultFile function for destructive operations
- Fix array concatenation bug in getQueryActions
- Return all vault files when search yields no results
- Update SearchVaultFiles description to clarify fallback behavior
- Improve error handling in writeFile to return error details
- Reorder system prompt sections for better clarity
This commit is contained in:
Andrew Beal 2025-10-14 22:42:52 +01:00
parent 86d59b18d2
commit 9f8d0b5ee8
9 changed files with 106 additions and 53 deletions

View file

@ -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
]);
}

View file

@ -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"]
}
}

View file

@ -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"]
}
}

View file

@ -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: {

View file

@ -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"]
}
}

View file

@ -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.**

View file

@ -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"

View file

@ -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<FileSystemService>(Services.FileSystemService);
public async performAIFunction(functionCall: AIFunctionCall): Promise<AIFunctionResponse> {
@ -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<object> {
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<object> {
private async readVaultFile(filePath: string): Promise<object> {
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<object> {
const result: boolean = await this.fileSystemService.writeFile(normalizePath(filePath), content);
return isBoolean(result) ? { success: result } : { success: false, error: result }
}
}

View file

@ -30,7 +30,7 @@ export class FileSystemService {
return null;
}
public async writeFile(filePath: string, content: string, allowAccessToPluginRoot: boolean = false): Promise<boolean> {
public async writeFile(filePath: string, content: string, allowAccessToPluginRoot: boolean = false): Promise<boolean | any> {
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;
}
}