mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add move_vault_folder tool and standardize schema naming
- Add MoveVaultFolder tool with supporting schema and enum - Rename schemas from *Schema to *ArgsSchema for consistency - Fix VaultService.move() to handle folder destinations properly - Update AIToolResponse to use AIToolResponsePayload wrapper - Update error message for non-existent move sources - Add MoveVaultFolder import and tool registration
This commit is contained in:
parent
16c9107606
commit
57fb5d3be3
10 changed files with 100 additions and 23 deletions
|
|
@ -20,6 +20,7 @@ import type { MistralStreamChunk, MistralToolDefinition, MistralMessage, Mistral
|
|||
import type { MistralFileService } from "./MistralFileService";
|
||||
import { Copy, replaceCopy } from "Enums/Copy";
|
||||
import { MistralAgent } from "./MistralAgent";
|
||||
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
|
||||
|
||||
export class Mistral extends BaseAIClass {
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ export class Mistral extends BaseAIClass {
|
|||
}
|
||||
const query = (toolCall.arguments as Record<string, string>).query ?? "";
|
||||
const result = await this.agent.search(query);
|
||||
return new AIToolResponse(toolCall.name, { result }, toolCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, new AIToolResponsePayload({ result }), toolCall.toolId);
|
||||
}
|
||||
|
||||
private async buildMessages(conversation: Conversation): Promise<MistralMessage[]> {
|
||||
|
|
|
|||
|
|
@ -38,24 +38,30 @@ export const MoveVaultFilesArgsSchema = z.object({
|
|||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const CreateVaultFolderSchema = z.object({
|
||||
export const CreateVaultFolderArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const DeleteVaultFolderSchema = z.object({
|
||||
export const DeleteVaultFolderArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
user_message: z.string(),
|
||||
confirm_deletion: z.boolean()
|
||||
});
|
||||
|
||||
export const MoveVaultFolderArgsSchema = z.object({
|
||||
source_path: z.string(),
|
||||
destination_path: z.string(),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const ListVaultFilesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
recursive: z.boolean(),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const GetWebViewerContentSchema = z.object({
|
||||
export const GetWebViewerContentArgsSchema = z.object({
|
||||
url_hint: z.string().optional(),
|
||||
format: z.enum(["text", "screenshot"]),
|
||||
user_message: z.string()
|
||||
|
|
@ -145,10 +151,11 @@ export type WriteVaultFileArgs = z.infer<typeof WriteVaultFileArgsSchema>;
|
|||
export type PatchVaultFileArgs = z.infer<typeof PatchVaultFileArgsSchema>;
|
||||
export type DeleteVaultFilesArgs = z.infer<typeof DeleteVaultFilesArgsSchema>;
|
||||
export type MoveVaultFilesArgs = z.infer<typeof MoveVaultFilesArgsSchema>;
|
||||
export type CreateVaultFolder = z.infer<typeof CreateVaultFolderSchema>;
|
||||
export type DeleteVaultFolder = z.infer<typeof DeleteVaultFolderSchema>;
|
||||
export type CreateVaultFolderArgs = z.infer<typeof CreateVaultFolderArgsSchema>;
|
||||
export type DeleteVaultFolderArgs = z.infer<typeof DeleteVaultFolderArgsSchema>;
|
||||
export type MoveVaultFolderArgs = z.infer<typeof MoveVaultFolderArgsSchema>;
|
||||
export type ListVaultFilesArgs = z.infer<typeof ListVaultFilesArgsSchema>;
|
||||
export type GetWebViewerContent = z.infer<typeof GetWebViewerContentSchema>;
|
||||
export type GetWebViewerContentArgs = z.infer<typeof GetWebViewerContentArgsSchema>;
|
||||
export type ExecuteWorkflowArgs = z.infer<typeof ExecuteWorkflowArgsSchema>;
|
||||
export type AskUserQuestionPlanningArgs = z.infer<typeof AskUserQuestionPlanningArgsSchema>;
|
||||
export type AskUserQuestionExecutionArgs = z.infer<typeof AskUserQuestionExecutionArgsSchema>;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { UpdateMemories } from "./Tools/UpdateMemories";
|
|||
import { CreateVaultFolder } from "./Tools/CreateVaultFolder";
|
||||
import { GetWebViewerContent } from "./Tools/GetWebViewerContent";
|
||||
import { DeleteVaultFolder } from "./Tools/DeleteVaultFolder";
|
||||
import { MoveVaultFolder } from "./Tools/MoveVaultFolder";
|
||||
|
||||
export abstract class AIToolDefinitions {
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ export abstract class AIToolDefinitions {
|
|||
|
||||
// Definitions list provides a list of function definitions that does not include any planning functions (used as reference in planning agent prompt)
|
||||
private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent,
|
||||
WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder, DeleteVaultFolder];
|
||||
WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder, DeleteVaultFolder, MoveVaultFolder];
|
||||
|
||||
public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean): IAIToolDefinition[] {
|
||||
this.isGated = false;
|
||||
|
|
@ -59,7 +60,8 @@ export abstract class AIToolDefinitions {
|
|||
DeleteVaultFiles,
|
||||
MoveVaultFiles,
|
||||
CreateVaultFolder,
|
||||
DeleteVaultFolder
|
||||
DeleteVaultFolder,
|
||||
MoveVaultFolder
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
36
AIClasses/ToolDefinitions/Tools/MoveVaultFolder.ts
Normal file
36
AIClasses/ToolDefinitions/Tools/MoveVaultFolder.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { AITool } from "Enums/AITool";
|
||||
import type { IAIToolDefinition } from "../IAIToolDefinition";
|
||||
|
||||
export const MoveVaultFolder: IAIToolDefinition = {
|
||||
name: AITool.MoveVaultFolder,
|
||||
description: `Moves or renames a folder within the vault to a new location.
|
||||
This operation moves the folder and all its contents, preserving the internal structure.
|
||||
|
||||
Call this function:
|
||||
- When reorganizing vault structure and moving a folder to a new parent directory
|
||||
- When renaming a folder for better organization
|
||||
- When consolidating related folders under a common parent
|
||||
|
||||
Do NOT use this function:
|
||||
- Before reading folder contents to confirm you're moving the correct folder (especially for large operations)
|
||||
- When moving individual files
|
||||
- When the destination folder already exists at that path`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
source_path: {
|
||||
type: "string",
|
||||
description: "The current path of the folder to move. Must be exact and point to an existing folder within the vault. Example: 'projects/old-name'"
|
||||
},
|
||||
destination_path: {
|
||||
type: "string",
|
||||
description: "The destination path for the folder (including the folder name). Supports renaming by providing a different folder name. Example: 'projects/new-name' or 'archive/projects/old-name'. Ensure the parent directory exists."
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
description: "A short message to be displayed to the user explaining why this folder is being moved. Examples: 'Moving your projects folder to the archive' (archiving), 'Renaming folder to match new naming convention' (renaming), or 'Reorganising your vault structure' (reorganizing)."
|
||||
}
|
||||
},
|
||||
required: ["source_path", "destination_path", "user_message"]
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export enum AITool {
|
|||
MoveVaultFiles = "move_vault_files",
|
||||
CreateVaultFolder = "create_vault_folder",
|
||||
DeleteVaultFolder = "delete_vault_folder",
|
||||
MoveVaultFolder = "move_vault_folder",
|
||||
ListVaultFiles = "list_vault_files",
|
||||
ReadMemories = "read_memories",
|
||||
UpdateMemories = "update_memories",
|
||||
|
|
|
|||
|
|
@ -29,9 +29,10 @@ import {
|
|||
PatchVaultFileArgsSchema,
|
||||
ReadMemoriesArgsSchema,
|
||||
UpdateMemoriesArgsSchema,
|
||||
CreateVaultFolderSchema,
|
||||
GetWebViewerContentSchema,
|
||||
DeleteVaultFolderSchema
|
||||
CreateVaultFolderArgsSchema,
|
||||
GetWebViewerContentArgsSchema,
|
||||
DeleteVaultFolderArgsSchema,
|
||||
MoveVaultFolderArgsSchema
|
||||
} from "AIClasses/Schemas/AIToolSchemas";
|
||||
|
||||
export class AIToolService {
|
||||
|
|
@ -133,7 +134,7 @@ export class AIToolService {
|
|||
}
|
||||
|
||||
case AITool.CreateVaultFolder: {
|
||||
const parseResult = CreateVaultFolderSchema.safeParse(toolCall.arguments);
|
||||
const parseResult = CreateVaultFolderArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
|
|
@ -145,7 +146,7 @@ export class AIToolService {
|
|||
}
|
||||
|
||||
case AITool.DeleteVaultFolder: {
|
||||
const parseResult = DeleteVaultFolderSchema.safeParse(toolCall.arguments);
|
||||
const parseResult = DeleteVaultFolderArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
|
|
@ -155,6 +156,18 @@ export class AIToolService {
|
|||
}
|
||||
return new AIToolResponse(toolCall.name, await this.deleteVaultFolder(parseResult.data.path, parseResult.data.confirm_deletion), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.MoveVaultFolder: {
|
||||
const parseResult = MoveVaultFolderArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.MoveVaultFolder}: ${parseResult.error.message}` }),
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(toolCall.name, await this.moveVaultFolder(parseResult.data.source_path, parseResult.data.destination_path), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.ListVaultFiles: {
|
||||
const parseResult = ListVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
|
|
@ -169,7 +182,7 @@ export class AIToolService {
|
|||
}
|
||||
|
||||
case AITool.GetWebViewerContent: {
|
||||
const parseResult = GetWebViewerContentSchema.safeParse(toolCall.arguments);
|
||||
const parseResult = GetWebViewerContentArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
|
|
@ -377,6 +390,14 @@ export class AIToolService {
|
|||
: new AIToolResponsePayload({ path: path, success: true });
|
||||
}
|
||||
|
||||
private async moveVaultFolder(sourcePath: string, destinationPath: string): Promise<AIToolResponsePayload> {
|
||||
const result = await this.fileSystemService.moveFile(sourcePath, destinationPath);
|
||||
if (result instanceof Error) {
|
||||
return new AIToolResponsePayload({ path: destinationPath, success: false, error: result.message });
|
||||
}
|
||||
return new AIToolResponsePayload({ path: destinationPath, success: true });
|
||||
}
|
||||
|
||||
private async listVaultFiles(path: string, recursive: boolean): Promise<AIToolResponsePayload> {
|
||||
const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive);
|
||||
return new AIToolResponsePayload(files.map(file => ({
|
||||
|
|
|
|||
|
|
@ -239,15 +239,22 @@ export class VaultService {
|
|||
const file = this.getAbstractFileByPath(sourcePath, allowAccessToPluginRoot);
|
||||
|
||||
if (file === null) {
|
||||
return Exception.new(`File does not exist: ${sourcePath}`);
|
||||
return Exception.new(`Move failed as source does not exist: ${sourcePath}`);
|
||||
}
|
||||
|
||||
const isFile = file instanceof TFile;
|
||||
|
||||
if (this.isExclusion(destinationPath, allowAccessToPluginRoot)) {
|
||||
return Exception.new(`Failed to rename "${sourcePath}" to "${destinationPath}", permission denied.`)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.createDirectories(destinationPath, allowAccessToPluginRoot)
|
||||
if (isFile) {
|
||||
await this.createDirectories(destinationPath, allowAccessToPluginRoot);
|
||||
} else {
|
||||
const parentPath = destinationPath.substring(0, destinationPath.lastIndexOf("/"));
|
||||
if (parentPath) await this.createDirectories(parentPath, allowAccessToPluginRoot);
|
||||
}
|
||||
await this.fileManager.renameFile(file, destinationPath);
|
||||
} catch (error) {
|
||||
Exception.log(error);
|
||||
|
|
|
|||
|
|
@ -345,16 +345,17 @@ describe('FileSystemService', () => {
|
|||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFile, true, true);
|
||||
});
|
||||
|
||||
it('should delete folder successfully (supports both files and folders)', async () => {
|
||||
it('should delete folder successfully', async () => {
|
||||
const mockFolder = createMockFolder('folder');
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFolder);
|
||||
mockVaultService.isExclusion = vi.fn().mockReturnValue(false);
|
||||
mockVaultService.delete = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await fileSystemService.deleteFile('folder');
|
||||
const result = await fileSystemService.deleteFolder('folder');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFolder, false, true);
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFolder, false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { RegisterSingleton, DeregisterAllServices } from '../../Services/Depende
|
|||
import { Services } from '../../Services/Services';
|
||||
import { AITool } from '../../Enums/AITool';
|
||||
import { AIToolResponse } from '../../AIClasses/ToolDefinitions/AIToolResponse';
|
||||
import { AIToolResponsePayload } from 'AIClasses/ToolDefinitions/AIToolResponsePayload';
|
||||
|
||||
/**
|
||||
* UNIT TESTS - OrchestrationAgent
|
||||
|
|
@ -36,7 +37,7 @@ describe('OrchestrationAgent - Unit Tests', () => {
|
|||
// Mock AIToolService
|
||||
mockAIToolService = {
|
||||
performAITool: vi.fn().mockResolvedValue(
|
||||
new AIToolResponse(AITool.SearchVaultFiles, { results: [] }, 'test-tool-id')
|
||||
new AIToolResponse(AITool.SearchVaultFiles, new AIToolResponsePayload({ results: [] }), 'test-tool-id')
|
||||
)
|
||||
};
|
||||
RegisterSingleton(Services.AIToolService, mockAIToolService);
|
||||
|
|
|
|||
|
|
@ -665,7 +665,7 @@ describe('VaultService - Integration Tests', () => {
|
|||
const result = await vaultService.move('Vaultkeeper AI/test.md', 'dest.md', false);
|
||||
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
expect((result as Error).message).toContain('File does not exist');
|
||||
expect((result as Error).message).toContain('Move failed as source does not exist');
|
||||
expect(mockFileManager.renameFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -675,7 +675,7 @@ describe('VaultService - Integration Tests', () => {
|
|||
const result = await vaultService.move('nonexistent.md', 'dest.md');
|
||||
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
expect((result as Error).message).toContain('File does not exist');
|
||||
expect((result as Error).message).toContain('Move failed as source does not exist');
|
||||
expect(mockFileManager.renameFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue