mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: restructure AI prompt and agent architecture for multi-agent planning support
- Move prompts from AIClasses to AIPrompts directory - Replace centralized IPrompt injection with direct property setters on IAIClass - Remove allowDestructiveActions parameter from streamRequest methods - Add toolDefinitions, systemPrompt, and userInstruction properties to IAIClass - Refactor AIFunctionDefinitions to static methods with agent-specific tool sets - Add planning agent function definitions (CreatePlan, Replan, CompleteStep, SubmitPlan) - Create AIControllerService to handle agent orchestration - Add execution plan related copy strings and replacement utility - Update all AI providers (Claude, Gemini, OpenAI) to use new architecture
This commit is contained in:
parent
989fab565d
commit
2de8109a74
38 changed files with 1355 additions and 323 deletions
|
|
@ -1,11 +1,9 @@
|
|||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import type { IAIClass } from "AIClasses/IAIClass";
|
||||
import type { IPrompt } from "AIClasses/IPrompt";
|
||||
import { type IStreamChunk } from "Services/StreamingService";
|
||||
import type { Conversation } from "Conversations/Conversation";
|
||||
import type { AIProvider } from "Enums/ApiProvider";
|
||||
import type { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
|
||||
import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFunctionDefinition";
|
||||
import type { ConversationContent } from "Conversations/ConversationContent";
|
||||
import type { Attachment } from "Conversations/Attachment";
|
||||
|
|
@ -22,31 +20,50 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
|
||||
protected readonly provider: AIProvider;
|
||||
protected readonly apiKey: string;
|
||||
protected readonly aiPrompt: IPrompt;
|
||||
protected readonly abortService: AbortService;
|
||||
protected readonly aiFileService: IAIFileService;
|
||||
protected readonly settingsService: SettingsService;
|
||||
protected readonly streamingService: StreamingService;
|
||||
protected readonly aiFunctionDefinitions: AIFunctionDefinitions;
|
||||
|
||||
private _systemPrompt: string = "";
|
||||
private _userInstruction: string = "";
|
||||
private _toolDefinitions: IAIFunctionDefinition[] = [];
|
||||
|
||||
protected constructor(provider: AIProvider) {
|
||||
this.provider = provider;
|
||||
this.aiPrompt = Resolve<IPrompt>(Services.IPrompt);
|
||||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
this.aiFileService = Resolve<IAIFileService>(Services.IAIFileService);
|
||||
this.settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
this.streamingService = Resolve<StreamingService>(Services.StreamingService);
|
||||
this.aiFunctionDefinitions = Resolve<AIFunctionDefinitions>(Services.AIFunctionDefinitions);
|
||||
|
||||
this.apiKey = this.settingsService.getApiKeyForProvider(provider);
|
||||
}
|
||||
|
||||
public set systemPrompt(systemPrompt: string) {
|
||||
this._systemPrompt = systemPrompt;
|
||||
}
|
||||
|
||||
public abstract streamRequest(
|
||||
conversation: Conversation,
|
||||
allowDestructiveActions: boolean,
|
||||
abortSignal?: AbortSignal
|
||||
): AsyncGenerator<IStreamChunk, void, unknown>;
|
||||
public get systemPrompt(): string {
|
||||
return this._systemPrompt;
|
||||
}
|
||||
|
||||
public set userInstruction(userInstruction: string) {
|
||||
this._userInstruction = userInstruction;
|
||||
}
|
||||
|
||||
public get userInstruction(): string {
|
||||
return this._userInstruction;
|
||||
}
|
||||
|
||||
public get toolDefinitions(): IAIFunctionDefinition[] {
|
||||
return this._toolDefinitions;
|
||||
}
|
||||
|
||||
public set toolDefinitions(toolDefinitions: IAIFunctionDefinition[]) {
|
||||
this._toolDefinitions = toolDefinitions;
|
||||
}
|
||||
|
||||
public abstract streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown>;
|
||||
|
||||
public abstract formatBinaryFiles(attachments: Attachment[]): string;
|
||||
|
||||
|
|
@ -123,13 +140,6 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
};
|
||||
}
|
||||
|
||||
protected async buildSystemPrompt(): Promise<string> {
|
||||
return [
|
||||
this.aiPrompt.systemInstruction(),
|
||||
await this.aiPrompt.userInstruction()
|
||||
].filter(s => s).join("\n\n");
|
||||
}
|
||||
|
||||
protected async processAttachments<T>(
|
||||
attachments: Attachment[],
|
||||
formatBinaryFiles: (attachments: Attachment[]) => string
|
||||
|
|
|
|||
|
|
@ -35,9 +35,8 @@ export class Claude extends BaseAIClass {
|
|||
super(AIProvider.Claude);
|
||||
}
|
||||
|
||||
public async* streamRequest(
|
||||
conversation: Conversation, allowDestructiveActions: boolean
|
||||
): AsyncGenerator<IStreamChunk, void, unknown> {
|
||||
public async* streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown> {
|
||||
|
||||
this.accumulatedFunctionName = null;
|
||||
this.accumulatedFunctionArgs = "";
|
||||
this.accumulatedFunctionId = null;
|
||||
|
|
@ -48,7 +47,7 @@ export class Claude extends BaseAIClass {
|
|||
}
|
||||
|
||||
// Build system prompt and convert to array with cache control
|
||||
const systemPromptText = await this.buildSystemPrompt();
|
||||
const systemPromptText = `${this.systemPrompt}\n\n${this.userInstruction}`;
|
||||
const systemPrompt: TextBlockParam[] = [
|
||||
{
|
||||
type: "text",
|
||||
|
|
@ -66,14 +65,8 @@ export class Claude extends BaseAIClass {
|
|||
max_uses: 5
|
||||
};
|
||||
|
||||
let tools: ToolUnion[] = [
|
||||
webSearchTool,
|
||||
...this.mapFunctionDefinitions(
|
||||
this.aiFunctionDefinitions.getQueryActions(allowDestructiveActions)
|
||||
)
|
||||
];
|
||||
|
||||
tools = this.addCacheControlToTools(tools);
|
||||
const tools: ToolUnion[] = this.addCacheControlToTools([
|
||||
webSearchTool, ...this.mapFunctionDefinitions(this.toolDefinitions)]);
|
||||
|
||||
const requestBody = {
|
||||
model: this.settingsService.settings.model,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Services } from "Services/Services";
|
|||
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
|
||||
import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
|
||||
import { Role } from "Enums/Role";
|
||||
import { NamePrompt } from "AIClasses/NamePrompt";
|
||||
import { NamePrompt } from "AIPrompts/NamePrompt";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { Exception } from "Helpers/Exception";
|
||||
|
|
|
|||
|
|
@ -6,9 +6,18 @@ import { DeleteVaultFiles } from "./Functions/DeleteVaultFiles";
|
|||
import { MoveVaultFiles } from "./Functions/MoveVaultFiles";
|
||||
import { ListVaultFiles } from "./Functions/ListVaultFiles";
|
||||
import { PatchVaultFile } from "./Functions/PatchVaultFile";
|
||||
import { CreatePlan } from "./Functions/CreatePlan";
|
||||
import { Replan } from "./Functions/Replan";
|
||||
import { CompleteStep } from "./Functions/CompleteStep";
|
||||
import { SubmitPlan } from "./Functions/SubmitPlan";
|
||||
|
||||
export class AIFunctionDefinitions {
|
||||
public getQueryActions(destructive: boolean): IAIFunctionDefinition[] {
|
||||
export abstract class AIFunctionDefinitions {
|
||||
|
||||
// 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, WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles];
|
||||
|
||||
// Definitions for the main agent
|
||||
public static agentDefinitions(destructive: boolean): IAIFunctionDefinition[] {
|
||||
let actions = [
|
||||
SearchVaultFiles,
|
||||
ReadVaultFiles,
|
||||
|
|
@ -20,10 +29,43 @@ export class AIFunctionDefinitions {
|
|||
WriteVaultFile,
|
||||
PatchVaultFile,
|
||||
DeleteVaultFiles,
|
||||
MoveVaultFiles
|
||||
MoveVaultFiles,
|
||||
CreatePlan
|
||||
]);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
// Definitions for the planning agent
|
||||
public static planningAgentDefinitions(): IAIFunctionDefinition[] {
|
||||
return [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, SubmitPlan];
|
||||
}
|
||||
|
||||
// Definitions for the main agent during plan execution
|
||||
public static agentExecutionDefinitions() {
|
||||
return [
|
||||
SearchVaultFiles,
|
||||
ReadVaultFiles,
|
||||
ListVaultFiles,
|
||||
WriteVaultFile,
|
||||
PatchVaultFile,
|
||||
DeleteVaultFiles,
|
||||
MoveVaultFiles,
|
||||
CompleteStep,
|
||||
Replan
|
||||
];
|
||||
}
|
||||
|
||||
public static compactSummaryForPlanningAgent(): string {
|
||||
return this.definitionsList.map(definition => {
|
||||
// Extract first line of description as brief purpose
|
||||
const description = definition.description.split('\n')[0].trim();
|
||||
return `| ${definition.name} | ${description} |`;
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
public static detailedAppendixForPlanningAgent(): string {
|
||||
return this.definitionsList.map(definition => JSON.stringify(definition, null, 2)).join('\n\n');
|
||||
}
|
||||
}
|
||||
30
AIClasses/FunctionDefinitions/Functions/CompleteStep.ts
Normal file
30
AIClasses/FunctionDefinitions/Functions/CompleteStep.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { AIFunction } from "Enums/AIFunction";
|
||||
import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
||||
|
||||
export const CompleteStep: IAIFunctionDefinition = {
|
||||
name: AIFunction.CompleteStep,
|
||||
description: `Marks a specific step in the current execution plan as completed.
|
||||
|
||||
Use this function to track your progress through a plan created by the planning agent.
|
||||
This helps maintain accurate state of which steps have been executed and provides
|
||||
visibility to the user about task progress.
|
||||
|
||||
Call this function:
|
||||
- Immediately after successfully completing a plan step
|
||||
- Before moving on to the next step in the plan
|
||||
- When a step's objectives have been fully satisfied
|
||||
|
||||
Do NOT use this function:
|
||||
- For steps that failed
|
||||
- For partial completion of a step`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
step_number: {
|
||||
type: "number",
|
||||
description: "The number of the step being marked as completed (1-indexed). This should correspond to the step number in the plan."
|
||||
}
|
||||
},
|
||||
required: ["step_number"]
|
||||
}
|
||||
}
|
||||
44
AIClasses/FunctionDefinitions/Functions/CreatePlan.ts
Normal file
44
AIClasses/FunctionDefinitions/Functions/CreatePlan.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { AIFunction } from "Enums/AIFunction";
|
||||
import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
||||
|
||||
export const CreatePlan: IAIFunctionDefinition = {
|
||||
name: AIFunction.CreatePlan,
|
||||
description: `Requests the planning agent to create a detailed, actionable plan for a high-level goal or task.
|
||||
|
||||
Use this function when you need strategic guidance on how to approach a complex
|
||||
or multi-step request. The planning agent will explore the vault context, analyze
|
||||
the requirements, and return a structured plan with specific steps for you to execute.
|
||||
|
||||
The planning agent specializes in:
|
||||
- Breaking down complex tasks into atomic, ordered steps
|
||||
- Conducting exploratory vault searches to inform the plan
|
||||
- Identifying dependencies and potential failure modes
|
||||
- Selecting appropriate tools and operations for each step
|
||||
- Adapting plan complexity to match the task requirements
|
||||
|
||||
Call this proactively when:
|
||||
- The user's request involves multiple operations or phases
|
||||
- You need to understand vault structure before acting
|
||||
- The optimal approach is unclear and requires analysis
|
||||
- The task requires coordination across multiple vault areas
|
||||
|
||||
Do NOT use for simple, single-step operations that don't require planning.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
goal: {
|
||||
type: "string",
|
||||
description: "The high-level goal or task to plan for. Should clearly describe what needs to be accomplished. Examples: 'Create a comprehensive summary of all machine learning notes', 'Reorganize project notes by topic', 'Research and compile information about quantum computing from the vault'"
|
||||
},
|
||||
context: {
|
||||
type: "string",
|
||||
description: "Optional additional context that may help inform the planning process. This can include: specific requirements, constraints, user preferences, relevant file paths already identified, or any other information that would help create a better plan. Examples: 'User prefers daily notes in YYYY-MM-DD format', 'Focus on notes created in the last month', 'Should preserve existing wiki-link structure'"
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
description: "A short message to be displayed to the user explaining why you're requesting a plan. Examples: 'Creating a plan to organize your project notes', 'Developing a strategy to compile your research on AI topics'"
|
||||
}
|
||||
},
|
||||
required: ["goal", "user_message"]
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,12 @@ import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
|||
|
||||
export const DeleteVaultFiles: IAIFunctionDefinition = {
|
||||
name: AIFunction.DeleteVaultFiles,
|
||||
description: `Permanently removes files and folders from the vault. Use this when the user explicitly
|
||||
requests to delete file(s) or folder(s), when a file or folder is no longer needed, or when removing outdated
|
||||
content. IMPORTANT: This action is irreversible - always confirm the exact file path(s)
|
||||
before deletion. Prefer archiving or moving files to a trash folder over permanent
|
||||
deletion when uncertain. Only call this after verifying the file(s) exist and confirming
|
||||
the user's intent to delete.`,
|
||||
description: `Permanently removes files and folders from the vault. Use this when the user explicitly
|
||||
requests to delete file(s) or folder(s), when a file or folder is no longer needed, or when removing outdated
|
||||
content. IMPORTANT: This action is irreversible - always confirm the exact file path(s)
|
||||
before deletion. Prefer archiving or moving files to a trash folder over permanent
|
||||
deletion when uncertain. Only call this after verifying the file(s) exist and confirming
|
||||
the user's intent to delete.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
|||
export const ListVaultFiles: IAIFunctionDefinition = {
|
||||
name: AIFunction.ListVaultFiles,
|
||||
description: `Lists files and directories in the vault's directory structure.
|
||||
Returns a structured view of the vault's organization including file names, paths, and directory hierarchy.
|
||||
Use this function when you need to:
|
||||
- List files in a specific directory or the entire vault
|
||||
- Get an overview of vault organization and structure
|
||||
- Browse available files and folders
|
||||
- Understand how notes are organized`,
|
||||
Returns a structured view of the vault's organization including file names, paths, and directory hierarchy.
|
||||
Use this function when you need to:
|
||||
- List files in a specific directory or the entire vault
|
||||
- Get an overview of vault organization and structure
|
||||
- Browse available files and folders
|
||||
- Understand how notes are organized`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
|||
export const MoveVaultFiles: IAIFunctionDefinition = {
|
||||
name: AIFunction.MoveVaultFiles,
|
||||
description: `Moves or renames one or more files within the vault to new locations.
|
||||
Use this when reorganizing vault structure, moving files between folders, renaming
|
||||
files for better organization, or consolidating related notes into appropriate
|
||||
directories. This operation preserves file content while updating paths and names.
|
||||
If renaming within the same folder, source and destination folders will be identical
|
||||
with only the filename changing. For safety, consider reading files first to confirm you're
|
||||
moving the correct content, especially when performing batch operations.`,
|
||||
Use this when reorganizing vault structure, moving files between folders, renaming
|
||||
files for better organization, or consolidating related notes into appropriate
|
||||
directories. This operation preserves file content while updating paths and names.
|
||||
If renaming within the same folder, source and destination folders will be identical
|
||||
with only the filename changing. For safety, consider reading files first to confirm you're
|
||||
moving the correct content, especially when performing batch operations.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ export const PatchVaultFile: IAIFunctionDefinition = {
|
|||
name: AIFunction.PatchVaultFile,
|
||||
description: `Apply targeted changes to an existing file in the vault by finding and replacing specific content.
|
||||
|
||||
This tool modifies specific sections of a file by matching exact content and replacing it with new content. It works by performing a direct string match and replace operation.
|
||||
This tool modifies specific sections of a file by matching exact content and replacing it with new content. It works by performing a direct string match and replace operation.
|
||||
|
||||
**When to use this tool:**
|
||||
- Making small, targeted edits to large files (a few lines changed)
|
||||
- Edits in the middle of a file where you have clear surrounding context
|
||||
- Simple line additions, deletions, or replacements with minimal changes
|
||||
- When you know the exact content that needs to be changed
|
||||
**When to use this tool:**
|
||||
- Making small, targeted edits to large files (a few lines changed)
|
||||
- Edits in the middle of a file where you have clear surrounding context
|
||||
- Simple line additions, deletions, or replacements with minimal changes
|
||||
- When you know the exact content that needs to be changed
|
||||
|
||||
**CRITICAL:** The content to match must be EXACTLY as it appears in the file - including all whitespace, indentation, blank lines, and line breaks.`,
|
||||
**CRITICAL:** The content to match must be EXACTLY as it appears in the file - including all whitespace, indentation, blank lines, and line breaks.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
@ -25,19 +25,19 @@ export const PatchVaultFile: IAIFunctionDefinition = {
|
|||
type: "string",
|
||||
description: `The exact content to find and replace in the file.
|
||||
|
||||
CRITICAL MATCHING REQUIREMENTS:
|
||||
- Must match the file content EXACTLY character-for-character
|
||||
- Include all whitespace, indentation, and line breaks exactly as they appear
|
||||
- Include enough context to make the match unique within the file
|
||||
- Typically include 2-3 lines before and after the change for context
|
||||
- Match complete lines/statements to avoid breaking code structure
|
||||
- Preserve all blank lines that exist in the original
|
||||
CRITICAL MATCHING REQUIREMENTS:
|
||||
- Must match the file content EXACTLY character-for-character
|
||||
- Include all whitespace, indentation, and line breaks exactly as they appear
|
||||
- Include enough context to make the match unique within the file
|
||||
- Typically include 2-3 lines before and after the change for context
|
||||
- Match complete lines/statements to avoid breaking code structure
|
||||
- Preserve all blank lines that exist in the original
|
||||
|
||||
WARNING: The replacement will fail if:
|
||||
- The content doesn't exist in the file
|
||||
- Whitespace/indentation doesn't match exactly
|
||||
- The match is ambiguous (appears multiple times in the file)
|
||||
- Line breaks are missing or incorrect`
|
||||
WARNING: The replacement will fail if:
|
||||
- The content doesn't exist in the file
|
||||
- Whitespace/indentation doesn't match exactly
|
||||
- The match is ambiguous (appears multiple times in the file)
|
||||
- Line breaks are missing or incorrect`
|
||||
},
|
||||
newContent: {
|
||||
type: "string",
|
||||
|
|
|
|||
|
|
@ -5,19 +5,19 @@ export const ReadVaultFiles: IAIFunctionDefinition = {
|
|||
name: AIFunction.ReadVaultFiles,
|
||||
description: `Reads and returns the complete content of one or more files from the vault.
|
||||
|
||||
**IMPORTANT: This function gives you the ability to SEE and ANALYZE images
|
||||
and PDFs. When users ask about image or PDF files, USE THIS FUNCTION to
|
||||
read them—do not claim you cannot see images.**
|
||||
**IMPORTANT: This function gives you the ability to SEE and ANALYZE images
|
||||
and PDFs. When users ask about image or PDF files, USE THIS FUNCTION to
|
||||
read them—do not claim you cannot see images.**
|
||||
|
||||
Call this when you need to access existing file 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.
|
||||
Call this when you need to access existing file 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.
|
||||
|
||||
For multiple files: Use when comparing content, gathering related context, or
|
||||
analyzing information across several documents.
|
||||
For multiple files: Use when comparing content, gathering related context, or
|
||||
analyzing information across several documents.
|
||||
|
||||
Supports text files (.md, .txt, etc.), images (.png, .jpg, .jpeg, etc), and PDFs (.pdf).`,
|
||||
Supports text files (.md, .txt, etc.), images (.png, .jpg, .jpeg, etc), and PDFs (.pdf).`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
58
AIClasses/FunctionDefinitions/Functions/Replan.ts
Normal file
58
AIClasses/FunctionDefinitions/Functions/Replan.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { AIFunction } from "Enums/AIFunction";
|
||||
import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
||||
|
||||
export const Replan: IAIFunctionDefinition = {
|
||||
name: AIFunction.Replan,
|
||||
description: `Requests the planning agent to revise or extend the current plan based on new information,
|
||||
obstacles encountered, or changing requirements during execution.
|
||||
|
||||
Use this function when the original plan needs adjustment due to:
|
||||
- Unexpected results from a step that require a different approach
|
||||
- Missing prerequisites discovered during execution
|
||||
- User feedback or clarifications that change requirements
|
||||
- Partial success that requires replanning remaining steps
|
||||
- New context or information that makes the current plan suboptimal
|
||||
|
||||
The planning agent will:
|
||||
- Review the original goal and current progress
|
||||
- Analyze what has been completed and what failed or changed
|
||||
- Create an updated plan that addresses the new situation
|
||||
- Preserve successful work while adapting remaining steps
|
||||
|
||||
Call this when:
|
||||
- A planned step fails and you need an alternative approach
|
||||
- Execution reveals the original plan was based on incorrect assumptions
|
||||
- The user provides new information mid-execution
|
||||
- You've completed part of the plan but the remaining steps are no longer valid
|
||||
|
||||
Do NOT use for:
|
||||
- Minor adjustments you can handle without planning assistance
|
||||
- Completely new tasks unrelated to the current plan (use CreatePlan instead)
|
||||
- Simple retries of failed operations`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
original_goal: {
|
||||
type: "string",
|
||||
description: "The original high-level goal from the initial plan. This provides continuity and helps the planning agent understand what we're ultimately trying to achieve."
|
||||
},
|
||||
completed_steps: {
|
||||
type: "string",
|
||||
description: "A summary of what has been successfully completed so far. Include any relevant outputs, created files, or state changes. Examples: 'Successfully read 5 notes from the Projects folder', 'Created summary.md with initial content'"
|
||||
},
|
||||
issue_encountered: {
|
||||
type: "string",
|
||||
description: "Description of what went wrong or what changed that necessitates replanning. Be specific about the problem. Examples: 'The notes folder structure is different than expected - notes are nested in subfolders by year', 'User clarified they only want notes from 2024', 'Write operation failed because file already exists'"
|
||||
},
|
||||
context: {
|
||||
type: "string",
|
||||
description: "Additional context including new information discovered, user preferences, constraints, or any other details that should inform the revised plan."
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
description: "A short message to be displayed to the user explaining why you're requesting a replan. Examples: 'Adjusting the plan based on the vault structure I found', 'Revising approach after encountering a conflict'"
|
||||
}
|
||||
},
|
||||
required: ["original_goal", "completed_steps", "issue_encountered", "context", "user_message"]
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
|||
export const SearchVaultFiles: IAIFunctionDefinition = {
|
||||
name: AIFunction.SearchVaultFiles,
|
||||
description: `Searches the content of all vault files using regex pattern matching.
|
||||
Returns files containing any of the search terms with contextual snippets showing where matches appear.
|
||||
Use this function when you need to:
|
||||
- Find specific concepts, keywords, or text within note contents
|
||||
- Locate content matching multiple patterns or phrases
|
||||
- Answer questions about what the user has written about a topic
|
||||
- Search across both file names and file contents simultaneously
|
||||
- Search for multiple related terms or variations in a single query`,
|
||||
Returns files containing any of the search terms with contextual snippets showing where matches appear.
|
||||
Use this function when you need to:
|
||||
- Find specific concepts, keywords, or text within note contents
|
||||
- Locate content matching multiple patterns or phrases
|
||||
- Answer questions about what the user has written about a topic
|
||||
- Search across both file names and file contents simultaneously
|
||||
- Search for multiple related terms or variations in a single query`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
38
AIClasses/FunctionDefinitions/Functions/SubmitPlan.ts
Normal file
38
AIClasses/FunctionDefinitions/Functions/SubmitPlan.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { AIFunction } from "Enums/AIFunction";
|
||||
import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
||||
|
||||
export const SubmitPlan: IAIFunctionDefinition = {
|
||||
name: AIFunction.SubmitPlan,
|
||||
description: `Submits an execution plan with ordered, actionable steps.
|
||||
Use this function after analyzing the goal and vault context to provide a structured
|
||||
plan that will be followed step-by-step. Each step should be clear and executable.`,
|
||||
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
steps: {
|
||||
type: "array",
|
||||
description: "Ordered array of execution steps. Each step represents an actionable task that moves toward the goal.",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
description: {
|
||||
type: "string",
|
||||
description: "Brief summary of what this step accomplishes (e.g., 'Search for ML notes', 'Create index file'). This is user-facing and should be concise."
|
||||
},
|
||||
instruction: {
|
||||
type: "string",
|
||||
description: "Detailed instructions for executing this step. Should be specific enough to guide the execution without ambiguity. Examples: 'Search vault for all notes with tag #machine-learning using search_vault_files', 'Create new file ML-Index.md in /Research folder with heading structure', 'Update frontmatter in daily note 2024-01-15 to add tag #reviewed'"
|
||||
},
|
||||
context: {
|
||||
type: "string",
|
||||
description: "Optional supporting context for this step. May include extracts from vault notes, relevant information gathered during planning, or other contextual details that will help execute this step effectively."
|
||||
}
|
||||
},
|
||||
required: ["description", "instruction"]
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["steps"]
|
||||
}
|
||||
};
|
||||
|
|
@ -4,11 +4,11 @@ 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.
|
||||
|
||||
**When to use this tool:**
|
||||
- Creating new notes, documents, or files from scratch
|
||||
- Completely rewriting a file's contents (when most/all content needs to change)
|
||||
- Generating new files from templates or structured data`,
|
||||
|
||||
**When to use this tool:**
|
||||
- Creating new notes, documents, or files from scratch
|
||||
- Completely rewriting a file's contents (when most/all content needs to change)
|
||||
- Generating new files from templates or structured data`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -76,9 +76,7 @@ export class Gemini extends BaseAIClass {
|
|||
super(AIProvider.Gemini);
|
||||
}
|
||||
|
||||
public async* streamRequest(
|
||||
conversation: Conversation, allowDestructiveActions: boolean
|
||||
): AsyncGenerator<IStreamChunk, void, unknown> {
|
||||
public async* streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown> {
|
||||
// next request should use web search only (gemini api doesn't support custom tooling and grounding at the same time)
|
||||
const requestWebSearch = this.accumulatedFunctionName == this.REQUEST_WEB_SEARCH;
|
||||
|
||||
|
|
@ -102,7 +100,7 @@ export class Gemini extends BaseAIClass {
|
|||
information, recent events, news, or facts that may have changed.
|
||||
After calling this, you will be able to perform web searches.`,
|
||||
},
|
||||
...this.mapFunctionDefinitions(this.aiFunctionDefinitions.getQueryActions(allowDestructiveActions)),
|
||||
...this.mapFunctionDefinitions(this.toolDefinitions),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +108,7 @@ export class Gemini extends BaseAIClass {
|
|||
system_instruction: {
|
||||
parts: [
|
||||
{
|
||||
text: this.aiPrompt.systemInstruction()
|
||||
text: this.systemPrompt
|
||||
},
|
||||
{
|
||||
text: `## IMPORTANT: Web Search Directive
|
||||
|
|
@ -120,7 +118,7 @@ export class Gemini extends BaseAIClass {
|
|||
- Recent news, events, or happenings.
|
||||
- Up-to-date prices, statistics, or factual data that is dynamic.
|
||||
- Any information where "current," "latest," or "today's" is implied or explicitly requested.
|
||||
|
||||
|
||||
When you need current information from the web, you *must* follow these steps:
|
||||
1. First call the \`request_web_search\` function with a clear and concise \`reasoning\` explaining why web search is needed.
|
||||
2. After calling this, you will be given access to Google Search.
|
||||
|
|
@ -128,7 +126,7 @@ export class Gemini extends BaseAIClass {
|
|||
4. Subsequent interactions will revert to standard function calls or general assistance as appropriate.`
|
||||
},
|
||||
{
|
||||
text: await this.aiPrompt.userInstruction()
|
||||
text: this.userInstruction
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Services } from "Services/Services";
|
|||
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
|
||||
import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
|
||||
import { Role } from "Enums/Role";
|
||||
import { NamePrompt } from "AIClasses/NamePrompt";
|
||||
import { NamePrompt } from "AIPrompts/NamePrompt";
|
||||
import type { GenerateContentResponse } from "@google/genai";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import type { IStreamChunk } from "Services/StreamingService";
|
||||
import type { Conversation } from "Conversations/Conversation";
|
||||
import type { Attachment } from "Conversations/Attachment";
|
||||
import type { IAIFunctionDefinition } from "./FunctionDefinitions/IAIFunctionDefinition";
|
||||
|
||||
export interface IAIClass {
|
||||
streamRequest(conversation: Conversation, allowDestructiveActions: boolean): AsyncGenerator<IStreamChunk, void, unknown>;
|
||||
set systemPrompt(systemPrompt: string);
|
||||
set userInstruction(userInstruction: string);
|
||||
set toolDefinitions(toolDefinitions: IAIFunctionDefinition[]);
|
||||
|
||||
streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown>;
|
||||
formatBinaryFiles(attachments: Attachment[]): string;
|
||||
}
|
||||
|
|
@ -28,24 +28,20 @@ export class OpenAI extends BaseAIClass {
|
|||
super(AIProvider.OpenAI);
|
||||
}
|
||||
|
||||
public async* streamRequest(
|
||||
conversation: Conversation, allowDestructiveActions: boolean
|
||||
): AsyncGenerator<IStreamChunk, void, unknown> {
|
||||
public async* streamRequest(conversation: Conversation): AsyncGenerator<IStreamChunk, void, unknown> {
|
||||
|
||||
// Refresh file cache only if conversation has attachments
|
||||
if (conversation.hasAttachments()) {
|
||||
await this.aiFileService.refreshCache();
|
||||
}
|
||||
|
||||
const systemPrompt = await this.buildSystemPrompt();
|
||||
const systemPrompt = `${this.systemPrompt}\n\n${this.userInstruction}`;
|
||||
|
||||
const input = await this.extractContents(conversation.contents);
|
||||
|
||||
const tools = [{
|
||||
type: "web_search"
|
||||
}, ...this.mapFunctionDefinitions(
|
||||
this.aiFunctionDefinitions.getQueryActions(allowDestructiveActions)
|
||||
)];
|
||||
}, ...this.mapFunctionDefinitions(this.toolDefinitions)];
|
||||
|
||||
const requestBody = {
|
||||
model: toProviderModel(this.settingsService.settings.model),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Services } from "Services/Services";
|
|||
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
|
||||
import { AIProvider, AIProviderURL, AIProviderModel } from "Enums/ApiProvider";
|
||||
import { Role } from "Enums/Role";
|
||||
import { NamePrompt } from "AIClasses/NamePrompt";
|
||||
import { NamePrompt } from "AIPrompts/NamePrompt";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type OpenAI from "openai";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
|
|
|
|||
|
|
@ -46,6 +46,32 @@ export const ListVaultFilesArgsSchema = z.object({
|
|||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const CreatePlanArgsSchema = z.object({
|
||||
goal: z.string(),
|
||||
context: z.string().optional(),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const ReplanArgsSchema = z.object({
|
||||
original_goal: z.string(),
|
||||
completed_steps: z.string(),
|
||||
issue_encountered: z.string(),
|
||||
context: z.string(),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const CompleteStepArgsSchema = z.object({
|
||||
step_number: z.number()
|
||||
});
|
||||
|
||||
export const SubmitPlanArgsSchema = z.object({
|
||||
steps: z.array(z.object({
|
||||
description: z.string(),
|
||||
instruction: z.string(),
|
||||
context: z.string().optional()
|
||||
}))
|
||||
});
|
||||
|
||||
// Infer TypeScript types from schemas
|
||||
export type SearchVaultFilesArgs = z.infer<typeof SearchVaultFilesArgsSchema>;
|
||||
export type ReadVaultFilesArgs = z.infer<typeof ReadVaultFilesArgsSchema>;
|
||||
|
|
@ -54,3 +80,7 @@ export type PatchVaultFileArgs = z.infer<typeof PatchVaultFileArgsSchema>;
|
|||
export type DeleteVaultFilesArgs = z.infer<typeof DeleteVaultFilesArgsSchema>;
|
||||
export type MoveVaultFilesArgs = z.infer<typeof MoveVaultFilesArgsSchema>;
|
||||
export type ListVaultFilesArgs = z.infer<typeof ListVaultFilesArgsSchema>;
|
||||
export type CreatePlanArgs = z.infer<typeof CreatePlanArgsSchema>;
|
||||
export type ReplanArgs = z.infer<typeof ReplanArgsSchema>;
|
||||
export type CompleteStepArgs = z.infer<typeof CompleteStepArgsSchema>;
|
||||
export type SubmitPlanArgs = z.infer<typeof SubmitPlanArgsSchema>;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import { Services } from "Services/Services";
|
|||
import { SystemInstruction } from "./SystemPrompt";
|
||||
import type { FileSystemService } from "Services/FileSystemService";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import { PlanningAgentSystemPrompt } from "AIPrompts/PlanningAgentSystemPrompt";
|
||||
|
||||
export interface IPrompt {
|
||||
systemInstruction(): string;
|
||||
planningInstruction(): string;
|
||||
userInstruction(): Promise<string>;
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +25,10 @@ export class AIPrompt implements IPrompt {
|
|||
return SystemInstruction;
|
||||
}
|
||||
|
||||
public planningInstruction(): string {
|
||||
return PlanningAgentSystemPrompt;
|
||||
}
|
||||
|
||||
public async userInstruction(): Promise<string> {
|
||||
const result = await this.fileSystemService.readFile(this.settingsService.settings.userInstruction, true);
|
||||
return result instanceof Error ? "" : result;
|
||||
197
AIPrompts/PlanningAgentSystemPrompt.ts
Normal file
197
AIPrompts/PlanningAgentSystemPrompt.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
|
||||
|
||||
export const PlanningAgentSystemPrompt: string = `
|
||||
# Obsidian Vault Planning Agent
|
||||
|
||||
You are a specialized planning agent within a multi-agent Obsidian vault assistant system. Your role is to analyze user requests, explore the vault's context, and create actionable, detailed plans that the main agent will execute.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
### 1. Request Analysis
|
||||
When you receive a planning request:
|
||||
- Parse the user's intent and identify the core objective
|
||||
- Determine the scope and complexity of the task
|
||||
- Identify which vault operations and tools will be needed
|
||||
- Consider dependencies between steps
|
||||
|
||||
### 2. Adaptive Planning Strategy
|
||||
|
||||
**Scale your planning effort to match query complexity:**
|
||||
|
||||
| Complexity | Exploration | Plan Detail | Example |
|
||||
|------------|-------------|-------------|---------|
|
||||
| Simple | 1-2 searches | 2-3 steps | "Create note about X" |
|
||||
| Moderate | 3-5 searches | 4-7 steps | "Organize notes on topic Y" |
|
||||
| Complex | 6-10 searches | 8-15 steps | "Research and synthesize Z across vault" |
|
||||
| Advanced | 10+ searches | 15+ steps with sub-plans | "Comprehensive vault restructuring" |
|
||||
|
||||
### 3. Deep Contextual Analysis
|
||||
**Before creating any plan, you MUST conduct thorough exploratory work:**
|
||||
|
||||
- **Vault Exploration**: Search the vault comprehensively to understand:
|
||||
- Existing relevant notes and their relationships
|
||||
- User's writing style, terminology, and organizational patterns
|
||||
- Naming conventions, folder structure, and tagging systems
|
||||
- Related concepts through [[wiki-links]] and backlinks
|
||||
|
||||
- **Knowledge Gap Analysis**: Identify what information exists vs. what's needed
|
||||
|
||||
- **Pattern Recognition**: Detect user preferences from existing vault structure
|
||||
|
||||
### 4. Progressive Search Methodology
|
||||
|
||||
**NEVER accept a failed search as final. Execute progressive tiers:**
|
||||
|
||||
**Tier 1 - Entity Extraction**: Extract key entities and search broadly
|
||||
**Tier 2 - Regex Patterns**: Use case-insensitive, wildcard, and alternative patterns
|
||||
**Tier 3 - Synonym Exploration**: Try variations, abbreviations, related terms
|
||||
**Tier 4 - Contextual Inference**: Check tags, backlinks, folder structures, related notes
|
||||
**Tier 5 - Cross-Reference**: Read found content to infer connections
|
||||
|
||||
**Example Search Progression:**
|
||||
1. Direct search: "machine learning"
|
||||
2. Regex: \`/(machine.?learning|ML|neural)/i\`
|
||||
3. Related: "AI", "deep learning", "models"
|
||||
4. Context: Check [[AI]] note for ML mentions
|
||||
5. Infer: Found in #technology tags or /projects/ai/ folder
|
||||
|
||||
**CRITICAL**: Always perform necessary exploratory work FIRST. A plan based on actual vault state is infinitely better than assumptions.
|
||||
|
||||
### 5. Plan Generation Principles
|
||||
|
||||
**Atomic Steps**: Break down the objective into clear, single-responsibility steps
|
||||
- Each step should have ONE clear action
|
||||
- Steps should be ordered to respect dependencies
|
||||
- Include conditional logic only when necessary
|
||||
|
||||
**Failure Anticipation**: Build robustness into your plans
|
||||
- Identify steps that might fail and why
|
||||
- Suggest fallback strategies for critical operations
|
||||
- Note when human intervention might be needed
|
||||
|
||||
## Available Tools
|
||||
|
||||
The main agent has access to the following vault operations. See the Appendix for complete parameter specifications.
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
${AIFunctionDefinitions.compactSummaryForPlanningAgent()}
|
||||
|
||||
**Important**:
|
||||
- Always use exact function names from the table above
|
||||
- Refer to the Appendix below for required parameters and detailed usage
|
||||
- Each function requires a \`user_message\` parameter to explain the action to the user
|
||||
|
||||
## Planning Architecture Patterns
|
||||
|
||||
### For Simple Tasks (1-3 steps)
|
||||
Use linear execution:
|
||||
\`\`\`
|
||||
1. Search vault for X
|
||||
2. Extract information Y
|
||||
3. Create note Z with findings
|
||||
\`\`\`
|
||||
|
||||
### For Medium Complexity (4-7 steps)
|
||||
Use sequential execution with checkpoints:
|
||||
\`\`\`
|
||||
1. [Discovery] Search for related notes
|
||||
2. [Discovery] Read and analyze key files
|
||||
3. [Checkpoint] Verify sufficient information exists
|
||||
4. [Synthesis] Extract and combine information
|
||||
5. [Creation] Generate new content
|
||||
6. [Validation] Verify output meets requirements
|
||||
\`\`\`
|
||||
|
||||
### For Complex Tasks (8+ steps)
|
||||
Use phased execution with validation:
|
||||
\`\`\`
|
||||
Phase 1: Information Gathering
|
||||
- Steps 1-3: Multi-angle vault searches
|
||||
- Validation: Confirm data completeness
|
||||
|
||||
Phase 2: Analysis
|
||||
- Steps 4-6: Process and synthesize information
|
||||
- Validation: Verify analysis quality
|
||||
|
||||
Phase 3: Execution
|
||||
- Steps 7-9: Create/modify vault content
|
||||
- Validation: Confirm deliverables meet spec
|
||||
\`\`\`
|
||||
|
||||
## Obsidian Vault-Specific Considerations
|
||||
|
||||
### Progressive Search Strategy
|
||||
When planning searches, incorporate the multi-tier approach:
|
||||
1. **Tier 1**: Direct entity/keyword search
|
||||
2. **Tier 2**: Regex pattern matching for variations
|
||||
3. **Tier 3**: Related content exploration (tags, backlinks)
|
||||
4. **Tier 4**: Contextual inference from similar notes
|
||||
|
||||
### Wiki-Link Integration
|
||||
Plans should preserve and create knowledge graph connections:
|
||||
- When referencing existing notes, use [[wiki-link]] notation
|
||||
- When creating new notes, specify links to related content
|
||||
- Plan for bidirectional linking where appropriate
|
||||
|
||||
### File Type Handling
|
||||
Account for different content types:
|
||||
- **Text notes**: Can be searched, created, updated inline
|
||||
- **Images/PDFs**: Must be read first, then referenced
|
||||
- **Complex structures**: May need multi-step processing
|
||||
|
||||
## Replanning Protocol
|
||||
|
||||
If the main agent requests a replan:
|
||||
1. Analyze the feedback provided
|
||||
2. Identify what went wrong and why
|
||||
3. Perform additional exploratory work if needed
|
||||
4. Generate an updated plan addressing the issues
|
||||
|
||||
DO NOT simply retry the same approach—learn from the failure.
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before returning any plan, verify:
|
||||
- [ ] Have I explored the vault to inform this plan?
|
||||
- [ ] Is each step atomic and clearly defined?
|
||||
- [ ] Are tool names and parameters exact and correct?
|
||||
- [ ] Do step dependencies make logical sense?
|
||||
- [ ] Have I anticipated likely failure modes?
|
||||
- [ ] Does the plan preserve Obsidian's knowledge graph through wiki-links?
|
||||
- [ ] Is the output structure valid and complete?
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
❌ Planning without vault exploration—always search first
|
||||
❌ Ignoring failure modes—plan for things going wrong
|
||||
❌ Over-complex plans for simple tasks—match complexity to need
|
||||
❌ Micromanaging execution instead of providing actionable guidance
|
||||
❌ Missing wiki-link opportunities—always preserve knowledge graph
|
||||
❌ Planning steps that don't align with available tools
|
||||
|
||||
## Example Planning Flow
|
||||
|
||||
**User Request**: "Create a summary of all my machine learning notes"
|
||||
|
||||
**Your Process**:
|
||||
1. Search vault to find ML-related notes
|
||||
2. Analyze the results to understand scope (10 notes? 100?)
|
||||
3. Read a sample to understand structure/content
|
||||
4. Design a plan that:
|
||||
- Searches comprehensively for ML notes
|
||||
- Reads each note systematically
|
||||
- Extracts key concepts and connections
|
||||
- Synthesizes findings
|
||||
- Creates a new summary note with proper wiki-links
|
||||
|
||||
**Remember**: You are the strategic intelligence of the system. The main agent executes; you ensure it executes optimally.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Complete Tool Specifications
|
||||
|
||||
Below are the complete function definitions available to the main agent, specified in JSON format as per Anthropic's documentation standards.
|
||||
|
||||
${AIFunctionDefinitions.detailedAppendixForPlanningAgent()}
|
||||
`;
|
||||
|
|
@ -31,7 +31,92 @@ User: "Create a note about today's meeting with Sarah"
|
|||
❌ Wrong: "I can create a note for you. Would you like me to proceed?"
|
||||
✅ Correct: [Immediately calls write_vault_file with appropriate content]
|
||||
|
||||
### 2. Historical Context Interpretation
|
||||
### 2. PLAN-THEN-EXECUTE ARCHITECTURE
|
||||
|
||||
**For complex tasks, separate strategic planning from tactical execution.**
|
||||
|
||||
You operate within a plan-and-execute architecture that improves task completion by:
|
||||
- **Explicit long-term planning**: Thinking through all steps required before acting
|
||||
- **Reduced cognitive load**: Allowing each step to focus on a narrow, well-defined objective
|
||||
- **Adaptive replanning**: Adjusting strategy when execution reveals new information or obstacles
|
||||
|
||||
#### When to Request Planning
|
||||
|
||||
**Request strategic planning for tasks that exhibit these characteristics:**
|
||||
|
||||
| Characteristic | Examples |
|
||||
|----------------|----------|
|
||||
| Multi-step coordination | "Reorganize my project notes by topic and create a summary index" |
|
||||
| Uncertain vault structure | "Find everything related to my startup idea and compile a report" |
|
||||
| Multiple vault areas | "Cross-reference my reading notes with my project plans" |
|
||||
| Dependency chains | "Create a content calendar based on my draft ideas" |
|
||||
| Research synthesis | "Give me an overview of all my notes on machine learning" |
|
||||
| Unclear optimal approach | "Help me prepare for my quarterly review using my vault" |
|
||||
|
||||
**Do NOT request planning for:**
|
||||
- Single-step operations (create one note, search for a term, read a file)
|
||||
- Clear, direct requests with obvious execution paths
|
||||
- Simple queries answerable from existing knowledge or a single search
|
||||
- Operations where you can immediately see the complete path to success
|
||||
|
||||
#### Decision Framework: Plan or Execute?
|
||||
|
||||
Ask yourself:
|
||||
1. **Can I complete this in 1-3 tool calls?** → Execute directly
|
||||
2. **Do I need to explore the vault structure first?** → Consider planning
|
||||
3. **Are there dependencies between steps?** → Consider planning
|
||||
4. **Could the optimal approach vary based on what I find?** → Consider planning
|
||||
5. **Is this a routine operation I've done before?** → Execute directly
|
||||
|
||||
**Default to direct execution. Elevate to planning only when complexity warrants it.**
|
||||
|
||||
#### Planning Workflow
|
||||
|
||||
When strategic guidance is needed:
|
||||
|
||||
1. **Request a plan** with a clear goal description and relevant context
|
||||
2. **Receive structured steps** from the planning agent, including:
|
||||
- Ordered sequence of actions
|
||||
- Dependencies between steps
|
||||
- Success criteria for each step
|
||||
- Potential failure modes and recovery strategies
|
||||
3. **Execute steps sequentially**, gathering ground truth at each stage
|
||||
4. **Monitor progress** against the plan's success criteria
|
||||
5. **Request replanning** if conditions change or obstacles emerge
|
||||
|
||||
### 3. ADAPTIVE REPLANNING
|
||||
|
||||
**Plans are hypotheses. Reality provides the test.**
|
||||
|
||||
Execution often reveals information that wasn't available during planning. Effective agents recognize when plans need adjustment rather than blindly following outdated instructions.
|
||||
|
||||
#### When to Request Replanning
|
||||
|
||||
| Trigger | Example Situation |
|
||||
|---------|-------------------|
|
||||
| **Unexpected results** | Search returned no results; expected folder structure doesn't exist |
|
||||
| **Failed prerequisites** | A note that should exist doesn't; permissions or access issues |
|
||||
| **Changed requirements** | User provides clarification that shifts the goal |
|
||||
| **Partial success** | Some steps completed but remaining steps are now invalid |
|
||||
| **Incorrect assumptions** | Plan assumed certain vault structure that doesn't match reality |
|
||||
| **New information** | Discovered content that suggests a better approach |
|
||||
|
||||
#### When NOT to Replan
|
||||
|
||||
- **Minor adjustments**: If you can adapt without strategic guidance, do so
|
||||
- **Simple retries**: If an operation failed but can succeed on retry
|
||||
- **Completed tasks**: Don't replan for a new, unrelated goal (request a fresh plan instead)
|
||||
- **Cosmetic issues**: Formatting or minor output adjustments don't need replanning
|
||||
|
||||
#### Replanning Best Practices
|
||||
|
||||
When requesting a replan:
|
||||
1. **Summarize completed work** clearly so it can be preserved
|
||||
2. **Diagnose the issue** specifically—what went wrong or changed?
|
||||
3. **Provide new context** discovered during execution
|
||||
4. **Maintain goal continuity** to ensure the original intent is honored
|
||||
|
||||
### 4. HISTORICAL CONTEXT INTERPRETATION
|
||||
|
||||
**Tool call history from previous sessions may appear with HTML comment markers.**
|
||||
|
||||
|
|
@ -43,12 +128,13 @@ These represent completed actions—NOT patterns to reproduce:
|
|||
|
||||
If you see JSON preceded by "Historical tool call/result", it documents a past action. Use your native function calling for new operations.
|
||||
|
||||
### 3. Request Completion
|
||||
### 5. 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
|
||||
- When executing a plan, complete all steps or explicitly pause at decision points
|
||||
|
||||
### 4. Wiki-Link Everything from the Vault
|
||||
### 6. 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
|
||||
|
|
@ -60,7 +146,7 @@ Examples:
|
|||
- "[[Sarah]] mentioned this in her meeting with [[John]]"
|
||||
- "This relates to your ideas about [[Machine Learning]] in [[Research Notes]]"
|
||||
|
||||
### 5. Vault-First Decision Framework
|
||||
### 7. Vault-First Decision Framework
|
||||
|
||||
**The cost of an unnecessary search is negligible. Missing relevant information is costly.**
|
||||
|
||||
|
|
@ -124,25 +210,37 @@ When searches return or reference images or PDFs:
|
|||
|
||||
## Multi-Tool Workflow Architecture
|
||||
|
||||
### Planning Phase (for Complex Queries)
|
||||
### Complexity Assessment
|
||||
|
||||
Before executing complex queries:
|
||||
1. **Intent Analysis**: Determine scope and complexity
|
||||
2. **Query Decomposition**: Break into searchable components
|
||||
3. **Strategy Design**: Plan search progression
|
||||
4. **Success Criteria**: Define what "complete" looks like
|
||||
Before executing, assess task complexity to determine the appropriate approach:
|
||||
|
||||
### Workflow Scaling
|
||||
| Complexity | Indicators | Approach |
|
||||
|------------|-----------|----------|
|
||||
| **Simple** | Single operation, clear target, 1-3 tool calls | Execute directly |
|
||||
| **Moderate** | Multiple searches, some ambiguity, 4-7 tool calls | Execute with fallback strategies |
|
||||
| **Complex** | Multi-phase, dependencies, exploration needed, 8-15 tool calls | Request strategic planning |
|
||||
| **Research** | Synthesis across many sources, 15+ tool calls | Request planning with research focus |
|
||||
|
||||
| Complexity | Operations | Example | Strategy |
|
||||
|------------|-----------|---------|----------|
|
||||
| Simple | 1-3 | "What did I write about React?" | Entity → Search → Done |
|
||||
| Moderate | 4-7 | "Find Docker and K8s notes" | Search → Regex fallback → Domain expansion |
|
||||
| Complex | 8-15 | "Compare microservices vs monoliths" | Full planning → Multi-search → Synthesis |
|
||||
| Research | 15+ | "Overview of my ML journey" | Comprehensive multi-source analysis |
|
||||
### Direct Execution (Simple & Moderate Tasks)
|
||||
|
||||
For straightforward operations:
|
||||
1. **Intent Analysis**: Understand what the user needs
|
||||
2. **Immediate Action**: Execute the appropriate tool calls
|
||||
3. **Progressive Fallback**: If initial approach fails, try alternatives
|
||||
4. **Complete Delivery**: Present findings with proper wiki-links
|
||||
|
||||
### Planned Execution (Complex & Research Tasks)
|
||||
|
||||
For tasks requiring coordination:
|
||||
1. **Request Planning**: Provide goal, context, and any known constraints
|
||||
2. **Receive Plan**: Get structured steps with dependencies and success criteria
|
||||
3. **Execute Sequentially**: Work through steps, gathering ground truth
|
||||
4. **Monitor & Adapt**: Check progress; request replan if needed
|
||||
5. **Synthesize Results**: Integrate findings across all steps
|
||||
|
||||
### Synthesis Phase
|
||||
|
||||
After multi-step execution:
|
||||
1. **Information Integration**: Combine results from all search attempts
|
||||
2. **Relationship Mapping**: Identify connections between sources
|
||||
3. **Universal Wiki-Linking**: Apply [[wiki-links]] to ALL vault references
|
||||
|
|
@ -179,19 +277,25 @@ Before executing complex queries:
|
|||
❌ Noting that a PDF/image exists without reading its contents when relevant
|
||||
❌ Asking users to describe images instead of reading them yourself
|
||||
❌ Saying "I cannot see/interpret images"
|
||||
❌ Requesting planning for simple, single-step operations
|
||||
❌ Blindly following a plan when execution reveals it's no longer valid
|
||||
❌ Replanning for minor issues you can handle directly
|
||||
|
||||
## Decision Framework
|
||||
|
||||
**Always ask yourself:**
|
||||
1. "Have I completed the user's request?" → Reflect on what can still be achieved
|
||||
2. "Am I using [[wiki-links]] for every vault reference?" → Always required
|
||||
3. "Could this information exist in the user's notes?" → Search vault first
|
||||
4. "Did my search fail? Have I tried all progressive tiers?" → Keep searching
|
||||
5. "Can I infer the answer from related content I found?" → Read and reason
|
||||
1. "Is this simple enough to execute directly, or do I need strategic planning?" → Default to direct execution
|
||||
2. "Have I completed the user's request?" → Reflect on what can still be achieved
|
||||
3. "Am I using [[wiki-links]] for every vault reference?" → Always required
|
||||
4. "Could this information exist in the user's notes?" → Search vault first
|
||||
5. "Did my search fail? Have I tried all progressive tiers?" → Keep searching
|
||||
6. "Can I infer the answer from related content I found?" → Read and reason
|
||||
7. "Has something changed that invalidates my current plan?" → Consider replanning
|
||||
8. "Am I adapting or do I need strategic guidance?" → Replan only for significant pivots
|
||||
|
||||
**When uncertain**: Always search the vault first. Always try alternative strategies before concluding "not found."
|
||||
**When uncertain**: Always search the vault first. Always try alternative strategies before concluding "not found." Scale complexity to match the query. Complete the full request before concluding.
|
||||
|
||||
---
|
||||
|
||||
**Core Philosophy**: Act first, explain after. Always use [[wiki-links]] for vault references. Be proactive with vault searches using progressive strategies—never give up after the first attempt. Scale search complexity to match the query. Complete the full request before concluding.
|
||||
**Core Philosophy**: Act first, explain after. Default to direct execution; elevate to planning only when task complexity warrants strategic coordination. Always use [[wiki-links]] for vault references. Be proactive with vault searches using progressive strategies—never give up after the first attempt. When executing plans, stay adaptive: replan when reality diverges from assumptions, but handle minor adjustments yourself.
|
||||
`;
|
||||
|
|
@ -11,6 +11,12 @@ export enum AIFunction {
|
|||
|
||||
// only used by gemini
|
||||
RequestWebSearch = "request_web_search",
|
||||
|
||||
// multi agent calls
|
||||
CreatePlan = "create_plan",
|
||||
Replan = "replan",
|
||||
SubmitPlan = "submit_plan",
|
||||
CompleteStep = "complete_step"
|
||||
}
|
||||
|
||||
export function fromString(functionName: string): AIFunction {
|
||||
|
|
@ -19,4 +25,8 @@ export function fromString(functionName: string): AIFunction {
|
|||
return enumValue as AIFunction;
|
||||
}
|
||||
Exception.throw(`Unknown function name: ${functionName}`);
|
||||
}
|
||||
|
||||
export function isAIFunction(value: unknown, aiFunction: AIFunction): value is AIFunction {
|
||||
return value === aiFunction;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { Exception } from "Helpers/Exception";
|
||||
|
||||
export enum Copy {
|
||||
// General Copy
|
||||
UserInstructions1 = "You can create custom ",
|
||||
|
|
@ -64,6 +66,41 @@ export enum Copy {
|
|||
|
||||
AIThoughtMessage = "Thinking...",
|
||||
|
||||
// Execution Plan Messages
|
||||
PlanningFailedError = "Planning failed. No execution plan was generated. Please consult with the user about how to proceed.",
|
||||
StepDoesNotExistError = "Step {stepNumber} does not exist in the execution plan. Valid step numbers are 1-{totalSteps}.",
|
||||
StepMustBeCompletedInOrderError = "Cannot complete step {stepNumber}. Step \"{incompletStep}\" is not yet completed. Steps must be completed in order.",
|
||||
StepCompletedWithNextStep = "Step {stepNumber} completed successfully. Now proceed with step {nextStepNumber}: {nextStepDescription}",
|
||||
AllStepsCompleted = "Step {stepNumber} completed successfully. All steps in the execution plan have been completed. Provide a final summary to the user based on the completed steps and overall success criteria.",
|
||||
MaxPlanningIterationsReached = "I've attempted multiple planning iterations but encountered persistent issues completing the task. It may need to be broken down further or requires additional clarification.",
|
||||
|
||||
// Execution Plan Request Templates
|
||||
ContextTags = `
|
||||
<CONTEXT>
|
||||
{context}
|
||||
</CONTEXT>`,
|
||||
ReplanRequestTemplate = `Plan execution has encountered an unexpected issue. Replan based on the following.
|
||||
|
||||
### Original Goal
|
||||
{originalGoal}
|
||||
|
||||
### Completed Steps
|
||||
{completedSteps}
|
||||
|
||||
### Issue Encountered
|
||||
{issueEncountered}
|
||||
|
||||
### Additional Context
|
||||
{context}`,
|
||||
IncompleteExecutionRequestTemplate = `Plan execution stopped before all steps were completed. Review the execution history and create a revised plan to complete the remaining work.
|
||||
|
||||
{completedSection}
|
||||
|
||||
{remainingSection}`,
|
||||
CompletedStepsHeader = "### Completed Steps",
|
||||
RemainingStepsHeader = "### Remaining Steps",
|
||||
NoSteps = "None",
|
||||
|
||||
// Help Modal Copy
|
||||
HelpModalAboutTitle = "About",
|
||||
HelpModalAboutContent = `#### About Vaultkeeper AI
|
||||
|
|
@ -516,4 +553,44 @@ Preferred programming language: {{language}}
|
|||
---
|
||||
|
||||
**Remember:** A good system prompt is clear, dense, and easy to understand, leaving no room for misinterpretation. Start simple, test thoroughly, and refine based on real results.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces placeholders in Copy strings with provided values.
|
||||
* Placeholders are denoted by curly braces: {placeholderName}
|
||||
*
|
||||
* @param copyString - The Copy enum string containing placeholders
|
||||
* @param replacements - Array of replacement values in the order they appear in the string
|
||||
* @returns The string with all placeholders replaced
|
||||
*
|
||||
* @example
|
||||
* replaceCopy(Copy.StepDoesNotExistError, ["5", "10"])
|
||||
* // Returns: "Step 5 does not exist in the execution plan. Valid step numbers are 1-10."
|
||||
*/
|
||||
export function replaceCopy(copyString: string, replacements: string[]): string {
|
||||
const placeholderRegex = /\{[^}]+\}/g;
|
||||
const placeholders = copyString.match(placeholderRegex);
|
||||
|
||||
if (!placeholders) {
|
||||
if (replacements.length > 0) {
|
||||
Exception.log(`No placeholders found in copy string, but ${replacements.length} replacement(s) provided.`);
|
||||
}
|
||||
return copyString;
|
||||
}
|
||||
|
||||
if (placeholders.length !== replacements.length) {
|
||||
Exception.log(`Placeholder count (${placeholders.length}) does not match replacement count (${replacements.length}). Using best effort.`);
|
||||
}
|
||||
|
||||
let result = copyString;
|
||||
let replacementIndex = 0;
|
||||
|
||||
result = result.replace(placeholderRegex, () => {
|
||||
if (replacementIndex < replacements.length) {
|
||||
return replacements[replacementIndex++];
|
||||
}
|
||||
return placeholders[replacementIndex++]; // Return original placeholder if no replacement available
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
5
Enums/ExecutionStatus.ts
Normal file
5
Enums/ExecutionStatus.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export enum ExecutionStatus {
|
||||
Pending = "pending",
|
||||
Active = "active",
|
||||
Completed = "completed"
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@ export function isKnownFileType(value: string): value is FileType {
|
|||
return Object.values(FileType).includes(value as FileType) && value !== FileType.UNKNOWN.toString();
|
||||
}
|
||||
|
||||
export function isFileType(value: string, fileType: FileType) {
|
||||
export function isFileType(value: unknown, fileType: FileType): value is FileType {
|
||||
return value === fileType.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
60
Helpers/ResponseHelper.ts
Normal file
60
Helpers/ResponseHelper.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
|
||||
// handle the rare event where a function call is also included in content (gemini sometimes does this)
|
||||
export function sanitizeFunctionCallContent(content: string, functionCall: AIFunctionCall | null): string {
|
||||
// Early returns for simple cases
|
||||
if (!functionCall || !content.trim()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// If content has no JSON-like characters, return as-is
|
||||
if (!content.includes('{') || !content.includes('}')) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
let sanitized = content;
|
||||
|
||||
// Step 1: Remove markdown code blocks that might contain the function call
|
||||
// Pattern matches ```json\n...\n``` or ```\n...\n```
|
||||
sanitized = sanitized.replace(/```(?:json)?\s*\n?([\s\S]*?)\n?```/g, (match: string, codeContent: string) => {
|
||||
// If the code block contains our function call, remove it entirely
|
||||
if (codeContent.trim() === functionCallString.trim()) {
|
||||
return '';
|
||||
}
|
||||
// Otherwise keep the code block
|
||||
return match;
|
||||
});
|
||||
|
||||
// Step 2: Remove exact JSON match (handles compact JSON)
|
||||
sanitized = sanitized.replace(functionCallString, '').trim();
|
||||
|
||||
// Step 3: Handle pretty-printed variations by normalizing both strings
|
||||
try {
|
||||
const functionCallObj: unknown = JSON.parse(functionCallString);
|
||||
const normalizedTarget = JSON.stringify(functionCallObj);
|
||||
|
||||
// Find and remove any JSON that matches when normalized
|
||||
// This regex finds JSON objects/arrays in the text
|
||||
const jsonPattern = /\{(?:[^{}]|(?:\{(?:[^{}]|(?:\{[^{}]*\}))*\}))*\}|\[(?:[^[\]]|(?:\[(?:[^[\]]|(?:\[[^[\]]*\]))*\]))*\]/g;
|
||||
|
||||
sanitized = sanitized.replace(jsonPattern, (match) => {
|
||||
try {
|
||||
const parsedMatch: unknown = JSON.parse(match);
|
||||
const normalizedMatch = JSON.stringify(parsedMatch);
|
||||
// Remove if it matches our function call when normalized
|
||||
return normalizedMatch === normalizedTarget ? '' : match;
|
||||
} catch {
|
||||
// If it's not valid JSON, keep it
|
||||
return match;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// If function call string isn't valid JSON, we've done what we can
|
||||
}
|
||||
|
||||
// Step 4: Clean up multiple consecutive whitespace/newlines left by removals
|
||||
sanitized = sanitized.replace(/\n{3,}/g, '\n\n').trim();
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
351
Services/AIControllerService.ts
Normal file
351
Services/AIControllerService.ts
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
import type { IAIClass } from "AIClasses/IAIClass";
|
||||
import { Services } from "./Services";
|
||||
import { Resolve } from "./DependencyService";
|
||||
import { Conversation } from "Conversations/Conversation";
|
||||
import type { IChatServiceCallbacks } from "./ChatService";
|
||||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
import type { AIFunctionService } from "./AIFunctionService";
|
||||
import { Copy, replaceCopy } from "Enums/Copy";
|
||||
import { sanitizeFunctionCallContent } from "Helpers/ResponseHelper";
|
||||
import type { IPrompt } from "AIPrompts/IPrompt";
|
||||
import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
|
||||
import { AIFunction, isAIFunction } from "Enums/AIFunction";
|
||||
import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { CompleteStepArgsSchema, CreatePlanArgsSchema, ReplanArgsSchema, SubmitPlanArgsSchema, type CreatePlanArgs, type ReplanArgs } from "AIClasses/Schemas/AIFunctionSchemas";
|
||||
import { ExecutionPlan } from "Types/ExecutionPlan";
|
||||
|
||||
export class AIControllerService {
|
||||
|
||||
private static readonly MAX_PLANNING_ITERATIONS = 3;
|
||||
|
||||
private ai: IAIClass | undefined;
|
||||
private readonly aiPrompt: IPrompt;
|
||||
private readonly aiFunctionService: AIFunctionService;
|
||||
|
||||
private planningConversation: Conversation;
|
||||
|
||||
public constructor() {
|
||||
this.aiPrompt = Resolve<IPrompt>(Services.IPrompt);
|
||||
this.aiFunctionService = Resolve<AIFunctionService>(Services.AIFunctionService);
|
||||
}
|
||||
|
||||
public resolveAIProvider() {
|
||||
this.ai = Resolve<IAIClass>(Services.IAIClass);
|
||||
}
|
||||
|
||||
public async runMainAgent(conversation: Conversation, allowDestructiveActions: boolean, callbacks: IChatServiceCallbacks) {
|
||||
if (!this.ai) { // this shouldn't ever happen
|
||||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
|
||||
// Setup initial prompts & tools
|
||||
this.ai.systemPrompt = this.aiPrompt.systemInstruction();
|
||||
this.ai.userInstruction = await this.aiPrompt.userInstruction();
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.agentDefinitions(allowDestructiveActions);
|
||||
|
||||
await this.runAgentLoop(conversation, callbacks, async (functionCall) => {
|
||||
const functionCallName = functionCall.name;
|
||||
if (isAIFunction(functionCallName, AIFunction.CreatePlan)) {
|
||||
const completedSuccessfully = await this.handlePlanningWorkflow(conversation, functionCall, callbacks);
|
||||
return { shouldExit: completedSuccessfully };
|
||||
}
|
||||
|
||||
this.updateThought({ functionCall, shouldContinue: false }, callbacks);
|
||||
const functionResponse = await this.aiFunctionService.performAIFunction(functionCall);
|
||||
conversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
});
|
||||
}
|
||||
|
||||
private async handlePlanningWorkflow(conversation: Conversation, functionCall: AIFunctionCall, callbacks: IChatServiceCallbacks): Promise<boolean> {
|
||||
if (!this.ai) { // this shouldn't ever happen
|
||||
Exception.throw("Error: No AI provider has been set!");
|
||||
}
|
||||
|
||||
const parseResult = CreatePlanArgsSchema.safeParse(functionCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for ${AIFunction.CreatePlan}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
));
|
||||
return false; // Return to main agent loop to handle the error
|
||||
}
|
||||
callbacks.onThoughtUpdate(parseResult.data.user_message);
|
||||
|
||||
// Orchestrate planning and execution loop (handles replanning)
|
||||
|
||||
let planningIteration = 0;
|
||||
let continueExecution = true;
|
||||
|
||||
this.planningConversation = new Conversation();
|
||||
this.planningConversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: this.preparePlanRequest(parseResult.data)
|
||||
}));
|
||||
|
||||
while (continueExecution && planningIteration < AIControllerService.MAX_PLANNING_ITERATIONS) {
|
||||
planningIteration++;
|
||||
|
||||
// Run planning agent to get execution plan
|
||||
this.ai.systemPrompt = this.aiPrompt.planningInstruction();
|
||||
this.ai.userInstruction = ""; // do not include user instruction
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.planningAgentDefinitions();
|
||||
|
||||
const executionPlan = await this.runPlanningAgent(this.planningConversation, callbacks);
|
||||
|
||||
// Run execution agent with the plan
|
||||
this.ai.systemPrompt = this.aiPrompt.systemInstruction();
|
||||
this.ai.userInstruction = await this.aiPrompt.userInstruction();
|
||||
this.ai.toolDefinitions = AIFunctionDefinitions.agentExecutionDefinitions();
|
||||
|
||||
const executionResult = await this.runExecutionAgent(conversation, functionCall, executionPlan, callbacks);
|
||||
|
||||
// Continue if either replanning was requested OR execution was incomplete
|
||||
continueExecution = executionResult.shouldReplan || executionResult.isIncomplete;
|
||||
|
||||
if (continueExecution && executionResult.replanData) {
|
||||
// Agent explicitly requested replan with context
|
||||
this.planningConversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: this.prepareReplanRequest(executionResult.replanData)
|
||||
}));
|
||||
} else if (continueExecution && executionResult.isIncomplete) {
|
||||
// Execution stopped prematurely - ask planner to revise
|
||||
this.planningConversation.contents.push(new ConversationContent({
|
||||
role: Role.User,
|
||||
content: this.prepareIncompleteExecutionRequest(executionPlan)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle max iterations reached
|
||||
if (planningIteration >= AIControllerService.MAX_PLANNING_ITERATIONS && continueExecution) {
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: Copy.MaxPlanningIterationsReached
|
||||
}));
|
||||
}
|
||||
|
||||
return true; // Planning and execution completed - terminate main agent
|
||||
}
|
||||
|
||||
private async runPlanningAgent(planningConversation: Conversation, callbacks: IChatServiceCallbacks): Promise<ExecutionPlan> {
|
||||
let capturedPlan: ExecutionPlan | null = null;
|
||||
|
||||
await this.runAgentLoop(planningConversation, callbacks, async (functionCall) => {
|
||||
const functionCallName = functionCall.name;
|
||||
|
||||
if (isAIFunction(functionCallName, AIFunction.SubmitPlan)) {
|
||||
const parseResult = SubmitPlanArgsSchema.safeParse(functionCall.arguments);
|
||||
capturedPlan = parseResult.success
|
||||
? new ExecutionPlan(parseResult.data)
|
||||
: new ExecutionPlan({ steps: [] });
|
||||
return { shouldExit: true }; // Exit once plan is submitted
|
||||
}
|
||||
|
||||
this.updateThought({ functionCall, shouldContinue: false }, callbacks);
|
||||
const functionResponse = await this.aiFunctionService.performAIFunction(functionCall);
|
||||
planningConversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
});
|
||||
|
||||
return capturedPlan ?? new ExecutionPlan({ steps: [] });
|
||||
}
|
||||
|
||||
// The 'execution agent' is still the main agent but given specific tools related to plan execution
|
||||
private async runExecutionAgent(conversation: Conversation, functionCall: AIFunctionCall,
|
||||
executionPlan: ExecutionPlan, callbacks: IChatServiceCallbacks
|
||||
): Promise<{ shouldReplan: boolean, isIncomplete: boolean, replanData?: ReplanArgs }> {
|
||||
// callback to ui with execution plan - gets reference to plan so auto updates when updated
|
||||
|
||||
conversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
executionPlan.toFunctionResponse(),
|
||||
functionCall.toolId
|
||||
));
|
||||
|
||||
let replanData: ReplanArgs | undefined;
|
||||
|
||||
await this.runAgentLoop(conversation, callbacks, async (functionCall) => {
|
||||
const functionCallName = functionCall.name;
|
||||
|
||||
if (isAIFunction(functionCallName, AIFunction.Replan)) {
|
||||
const parseResult = ReplanArgsSchema.safeParse(functionCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCallName,
|
||||
{ error: `Invalid arguments for ${AIFunction.Replan}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
// Capture replan data and exit execution loop to trigger replanning
|
||||
replanData = parseResult.data;
|
||||
return { shouldExit: true };
|
||||
}
|
||||
|
||||
if (isAIFunction(functionCallName, AIFunction.CompleteStep)) {
|
||||
const parseResult = CompleteStepArgsSchema.safeParse(functionCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIFunctionResponse(
|
||||
functionCallName,
|
||||
{ error: `Invalid arguments for ${AIFunction.CompleteStep}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
const functionResponse = new AIFunctionResponse(
|
||||
functionCallName,
|
||||
executionPlan.completeExecutionStep(parseResult.data.step_number),
|
||||
functionCall.toolId
|
||||
);
|
||||
conversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
}
|
||||
|
||||
this.updateThought({ functionCall, shouldContinue: false }, callbacks);
|
||||
const functionResponse = await this.aiFunctionService.performAIFunction(functionCall);
|
||||
conversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
});
|
||||
|
||||
const isIncomplete = !executionPlan.completed();
|
||||
return { shouldReplan: replanData !== undefined, isIncomplete, replanData };
|
||||
}
|
||||
|
||||
private async runAgentLoop(conversation: Conversation,callbacks: IChatServiceCallbacks,
|
||||
handleFunctionCall: (functionCall: AIFunctionCall) => Promise<{ shouldExit: boolean }>
|
||||
): Promise<void> {
|
||||
let response = await this.streamRequestResponse(this.ensureCorrectConversationStructure(conversation), callbacks);
|
||||
|
||||
while (response.functionCall || response.shouldContinue) {
|
||||
if (response.functionCall) {
|
||||
const result = await handleFunctionCall(response.functionCall);
|
||||
if (result.shouldExit) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
callbacks.onThoughtUpdate(Copy.AIThoughtMessage);
|
||||
}
|
||||
|
||||
response = await this.streamRequestResponse(this.ensureCorrectConversationStructure(conversation), callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
private updateThought(response: { functionCall: AIFunctionCall | null, shouldContinue: boolean }, callbacks: IChatServiceCallbacks) {
|
||||
const userMessage = response.functionCall?.arguments.user_message;
|
||||
if (userMessage && typeof userMessage === "string") {
|
||||
callbacks.onThoughtUpdate(userMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureCorrectConversationStructure(conversation: Conversation): Conversation {
|
||||
// Check if the last message is from the assistant to prevent assistant-to-assistant structure
|
||||
// This can happen when the assistant's last message had no function call and the user sends a new request
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
// Insert a hidden "Continue" message to maintain proper conversation structure
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private async streamRequestResponse(conversation: Conversation, callbacks: IChatServiceCallbacks
|
||||
): Promise<{ functionCall: AIFunctionCall | null, shouldContinue: boolean }> {
|
||||
if (!this.ai) { // this should never happen
|
||||
return { functionCall: null, shouldContinue: false };
|
||||
}
|
||||
|
||||
const conversationContent = new ConversationContent({ role: Role.Assistant });
|
||||
conversation.contents.push(conversationContent);
|
||||
|
||||
let accumulatedContent = "";
|
||||
let capturedFunctionCall: AIFunctionCall | null = null;
|
||||
let capturedShouldContinue = false;
|
||||
|
||||
for await (const chunk of this.ai.streamRequest(conversation)) {
|
||||
if (chunk.error && chunk.errorType) {
|
||||
conversationContent.content = chunk.error;
|
||||
conversationContent.errorType = chunk.errorType;
|
||||
callbacks.onStreamingUpdate(null);
|
||||
break;
|
||||
}
|
||||
|
||||
if (chunk.functionCall) {
|
||||
capturedFunctionCall = chunk.functionCall;
|
||||
}
|
||||
|
||||
if (chunk.shouldContinue) {
|
||||
capturedShouldContinue = true;
|
||||
}
|
||||
|
||||
if (chunk.content) {
|
||||
accumulatedContent += chunk.content;
|
||||
|
||||
conversationContent.content = accumulatedContent;
|
||||
if (accumulatedContent.trim() !== "") {
|
||||
callbacks.onThoughtUpdate(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.isComplete) {
|
||||
const sanitizedContent = sanitizeFunctionCallContent(accumulatedContent, capturedFunctionCall);
|
||||
|
||||
if (sanitizedContent.trim() === "" && !capturedFunctionCall) {
|
||||
conversation.contents.pop();
|
||||
} else {
|
||||
conversationContent.content = sanitizedContent;
|
||||
if (capturedFunctionCall) {
|
||||
conversationContent.functionCall = capturedFunctionCall.toConversationString();
|
||||
if (capturedFunctionCall.thoughtSignature) {
|
||||
conversationContent.thoughtSignature = capturedFunctionCall.thoughtSignature;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationContent.content?.trim() !== "") {
|
||||
callbacks.onStreamingUpdate(conversationContent.timestamp.getTime().toString());
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.onStreamingUpdate(null);
|
||||
|
||||
return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue };
|
||||
}
|
||||
|
||||
private preparePlanRequest(input: CreatePlanArgs): string {
|
||||
const context = input.context ? replaceCopy(Copy.ContextTags, [input.context]) : "";
|
||||
return input.goal + context;
|
||||
}
|
||||
|
||||
private prepareReplanRequest(input: ReplanArgs): string {
|
||||
return replaceCopy(Copy.ReplanRequestTemplate, [
|
||||
input.original_goal,
|
||||
input.completed_steps,
|
||||
input.issue_encountered,
|
||||
input.context
|
||||
]);
|
||||
}
|
||||
|
||||
private prepareIncompleteExecutionRequest(executionPlan: ExecutionPlan): string {
|
||||
const { completed, remaining } = executionPlan.getStatusSummary();
|
||||
const completedSection = completed.length > 0
|
||||
? `${Copy.CompletedStepsHeader}\n${completed.join("\n")}`
|
||||
: `${Copy.CompletedStepsHeader}\n${Copy.NoSteps}`;
|
||||
const remainingSection = remaining.length > 0
|
||||
? `${Copy.RemainingStepsHeader}\n${remaining.join("\n")}`
|
||||
: `${Copy.RemainingStepsHeader}\n${Copy.NoSteps}`;
|
||||
|
||||
return replaceCopy(Copy.IncompleteExecutionRequestTemplate, [
|
||||
completedSection,
|
||||
remainingSection
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Resolve } from "./DependencyService";
|
||||
import { Services } from "./Services";
|
||||
import type { FileSystemService } from "./FileSystemService";
|
||||
import { AIFunction } from "Enums/AIFunction";
|
||||
import { AIFunction, fromString } from "Enums/AIFunction";
|
||||
import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
|
||||
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
import type { ISearchMatch } from "../Helpers/SearchTypes";
|
||||
|
|
@ -37,7 +37,7 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for SearchVaultFiles: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.SearchVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for ReadVaultFiles: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.ReadVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for WriteVaultFile: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.WriteVaultFile}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for PatchVaultFile: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.PatchVaultFile}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for DeleteVaultFiles: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.DeleteVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for MoveVaultFiles: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.MoveVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
|
|
@ -109,22 +109,32 @@ export class AIFunctionService {
|
|||
if (!parseResult.success) {
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
{ error: `Invalid arguments for ListVaultFiles: ${parseResult.error.message}` },
|
||||
{ error: `Invalid arguments for ${AIFunction.ListVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIFunctionResponse(functionCall.name, await this.ListVaultFiles(parseResult.data.path, parseResult.data.recursive), functionCall.toolId);
|
||||
}
|
||||
|
||||
// this is only used by gemini
|
||||
// This is only used by gemini
|
||||
case AIFunction.RequestWebSearch:
|
||||
return new AIFunctionResponse(functionCall.name, {}, functionCall.toolId)
|
||||
|
||||
|
||||
// multi-agent functions are handled elsewhere - this shouldn't ever get hit
|
||||
case AIFunction.CreatePlan:
|
||||
case AIFunction.Replan:
|
||||
case AIFunction.SubmitPlan:
|
||||
case AIFunction.CompleteStep: {
|
||||
Exception.throw(`Multi-agent function ${functionCall.name} should not be handled by AIFunctionService`);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
const error = `Unknown function request ${functionCall.name as string}`
|
||||
const functionCallName = fromString(functionCall.name);
|
||||
const error = `Unknown function request ${functionCallName}`
|
||||
Exception.log(error);
|
||||
return new AIFunctionResponse(
|
||||
functionCall.name,
|
||||
functionCallName,
|
||||
{ error: error },
|
||||
functionCall.toolId
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export class AbortService {
|
|||
public reset = () => this.initialiseAbortController(); // semantic alias for initialiseAbortController
|
||||
|
||||
public initialiseAbortController(): void {
|
||||
this.abortController.abort();
|
||||
this.abort();
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
import { Semaphore } from "Helpers/Semaphore";
|
||||
import { Resolve } from "./DependencyService";
|
||||
import { Services } from "./Services";
|
||||
import type { IAIClass } from "AIClasses/IAIClass";
|
||||
import type { ConversationFileSystemService } from "./ConversationFileSystemService";
|
||||
import type { AIFunctionService } from "./AIFunctionService";
|
||||
import type { ConversationNamingService } from "./ConversationNamingService";
|
||||
import { Conversation } from "Conversations/Conversation";
|
||||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
|
||||
import { Notice } from "obsidian";
|
||||
import type { EventService } from "./EventService";
|
||||
import { Event } from "Enums/Event";
|
||||
import { AbortService } from "./AbortService";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import type { Attachment } from "Conversations/Attachment";
|
||||
import { Reference } from "Conversations/Reference";
|
||||
import type { WorkSpaceService } from "./WorkSpaceService";
|
||||
import type { AIControllerService } from "./AIControllerService";
|
||||
|
||||
export interface IChatServiceCallbacks {
|
||||
onSubmit: () => void;
|
||||
|
|
@ -28,9 +25,9 @@ export interface IChatServiceCallbacks {
|
|||
}
|
||||
|
||||
export class ChatService {
|
||||
private ai: IAIClass | undefined;
|
||||
|
||||
private aiControllerService: AIControllerService;
|
||||
private conversationService: ConversationFileSystemService;
|
||||
private aiFunctionService: AIFunctionService;
|
||||
private namingService: ConversationNamingService;
|
||||
private workSpaceService: WorkSpaceService;
|
||||
private eventService: EventService;
|
||||
|
|
@ -40,8 +37,8 @@ export class ChatService {
|
|||
private semaphoreHeld: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.aiControllerService = Resolve<AIControllerService>(Services.AIControllerService);
|
||||
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
|
||||
this.aiFunctionService = Resolve<AIFunctionService>(Services.AIFunctionService);
|
||||
this.namingService = Resolve<ConversationNamingService>(Services.ConversationNamingService);
|
||||
this.workSpaceService = Resolve<WorkSpaceService>(Services.WorkSpaceService);
|
||||
this.eventService = Resolve<EventService>(Services.EventService);
|
||||
|
|
@ -51,10 +48,6 @@ export class ChatService {
|
|||
|
||||
public onNameChanged: ((name: string) => void) | undefined = undefined;
|
||||
|
||||
public resolveAIProvider() {
|
||||
this.ai = Resolve<IAIClass>(Services.IAIClass);
|
||||
}
|
||||
|
||||
public async submit(conversation: Conversation, allowDestructiveActions: boolean, userRequest: string, formattedRequest: string, attachments: Attachment[], callbacks: IChatServiceCallbacks) {
|
||||
if (!await this.semaphore.wait()) {
|
||||
return;
|
||||
|
|
@ -100,23 +93,7 @@ export class ChatService {
|
|||
await this.namingService.requestName(conversation, formattedRequest, this.onNameChanged);
|
||||
}
|
||||
|
||||
// Process AI responses and function calls
|
||||
let response = await this.streamRequestResponse(this.ensureCorrectConversationStructure(conversation), allowDestructiveActions, callbacks);
|
||||
while (response.functionCall || response.shouldContinue) {
|
||||
if (response.functionCall) {
|
||||
const userMessage = response.functionCall.arguments.user_message;
|
||||
if (userMessage && typeof userMessage === "string") {
|
||||
callbacks.onThoughtUpdate(userMessage);
|
||||
}
|
||||
|
||||
const functionResponse = await this.aiFunctionService.performAIFunction(response.functionCall);
|
||||
conversation.addFunctionResponse(functionResponse);
|
||||
} else {
|
||||
callbacks.onThoughtUpdate(Copy.AIThoughtMessage);
|
||||
}
|
||||
|
||||
response = await this.streamRequestResponse(this.ensureCorrectConversationStructure(conversation), allowDestructiveActions, callbacks);
|
||||
}
|
||||
await this.aiControllerService.runMainAgent(conversation, allowDestructiveActions, callbacks);
|
||||
});
|
||||
} catch (error) {
|
||||
if (AbortService.isAbortError(error)) {
|
||||
|
|
@ -142,152 +119,15 @@ export class ChatService {
|
|||
this.eventService.trigger(Event.DiffClosed);
|
||||
}
|
||||
|
||||
private requestWithContext(request: string) {
|
||||
const activeFile = this.workSpaceService.getActiveFile();
|
||||
return activeFile ? `${request}\nUser current active file: "${activeFile.path}"` : request;
|
||||
}
|
||||
|
||||
private async saveConversation(conversation: Conversation) {
|
||||
const result = await this.conversationService.saveConversation(conversation);
|
||||
if (result instanceof Error) {
|
||||
new Notice(`Failed to save conversation data for '${conversation.title}'`);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureCorrectConversationStructure(conversation: Conversation): Conversation {
|
||||
// Check if the last message is from the assistant to prevent assistant-to-assistant structure
|
||||
// This can happen when the assistant's last message had no function call and the user sends a new request
|
||||
if (conversation.contents.length > 0) {
|
||||
const lastMessage = conversation.contents[conversation.contents.length - 1];
|
||||
if (lastMessage.role === Role.Assistant) {
|
||||
// Insert a hidden "Continue" message to maintain proper conversation structure
|
||||
conversation.contents.push(ConversationContent.safeContinue());
|
||||
}
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private async streamRequestResponse(
|
||||
conversation: Conversation, allowDestructiveActions: boolean, callbacks: IChatServiceCallbacks
|
||||
): Promise<{ functionCall: AIFunctionCall | null, shouldContinue: boolean }> {
|
||||
if (!this.ai) { // this should never happen
|
||||
return { functionCall: null, shouldContinue: false };;
|
||||
}
|
||||
|
||||
const conversationContent = new ConversationContent({ role: Role.Assistant });
|
||||
conversation.contents.push(conversationContent);
|
||||
|
||||
let accumulatedContent = "";
|
||||
let capturedFunctionCall: AIFunctionCall | null = null;
|
||||
let capturedShouldContinue = false;
|
||||
|
||||
for await (const chunk of this.ai.streamRequest(conversation, allowDestructiveActions)) {
|
||||
if (chunk.error && chunk.errorType) {
|
||||
conversationContent.content = chunk.error;
|
||||
conversationContent.errorType = chunk.errorType;
|
||||
callbacks.onStreamingUpdate(null);
|
||||
break;
|
||||
}
|
||||
|
||||
if (chunk.functionCall) {
|
||||
capturedFunctionCall = chunk.functionCall;
|
||||
}
|
||||
|
||||
if (chunk.shouldContinue) {
|
||||
capturedShouldContinue = true;
|
||||
}
|
||||
|
||||
if (chunk.content) {
|
||||
accumulatedContent += chunk.content;
|
||||
|
||||
conversationContent.content = accumulatedContent;
|
||||
if (accumulatedContent.trim() !== "") {
|
||||
callbacks.onThoughtUpdate(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.isComplete) {
|
||||
const sanitizedContent = this.sanitizeFunctionCallContent(accumulatedContent, capturedFunctionCall);
|
||||
|
||||
if (sanitizedContent.trim() === "" && !capturedFunctionCall) {
|
||||
conversation.contents.pop();
|
||||
} else {
|
||||
conversationContent.content = sanitizedContent;
|
||||
if (capturedFunctionCall) {
|
||||
conversationContent.functionCall = capturedFunctionCall.toConversationString();
|
||||
if (capturedFunctionCall.thoughtSignature) {
|
||||
conversationContent.thoughtSignature = capturedFunctionCall.thoughtSignature;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationContent.content?.trim() !== "") {
|
||||
callbacks.onStreamingUpdate(conversationContent.timestamp.getTime().toString());
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.onStreamingUpdate(null);
|
||||
|
||||
return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue };
|
||||
}
|
||||
|
||||
// handle the rare event where a function call is also included in content (gemini sometimes does this)
|
||||
private sanitizeFunctionCallContent(content: string, functionCall: AIFunctionCall | null): string {
|
||||
// Early returns for simple cases
|
||||
if (!functionCall || !content.trim()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// If content has no JSON-like characters, return as-is
|
||||
if (!content.includes('{') || !content.includes('}')) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
let sanitized = content;
|
||||
|
||||
// Step 1: Remove markdown code blocks that might contain the function call
|
||||
// Pattern matches ```json\n...\n``` or ```\n...\n```
|
||||
sanitized = sanitized.replace(/```(?:json)?\s*\n?([\s\S]*?)\n?```/g, (match: string, codeContent: string) => {
|
||||
// If the code block contains our function call, remove it entirely
|
||||
if (codeContent.trim() === functionCallString.trim()) {
|
||||
return '';
|
||||
}
|
||||
// Otherwise keep the code block
|
||||
return match;
|
||||
});
|
||||
|
||||
// Step 2: Remove exact JSON match (handles compact JSON)
|
||||
sanitized = sanitized.replace(functionCallString, '').trim();
|
||||
|
||||
// Step 3: Handle pretty-printed variations by normalizing both strings
|
||||
try {
|
||||
const functionCallObj: unknown = JSON.parse(functionCallString);
|
||||
const normalizedTarget = JSON.stringify(functionCallObj);
|
||||
|
||||
// Find and remove any JSON that matches when normalized
|
||||
// This regex finds JSON objects/arrays in the text
|
||||
const jsonPattern = /\{(?:[^{}]|(?:\{(?:[^{}]|(?:\{[^{}]*\}))*\}))*\}|\[(?:[^[\]]|(?:\[(?:[^[\]]|(?:\[[^[\]]*\]))*\]))*\]/g;
|
||||
|
||||
sanitized = sanitized.replace(jsonPattern, (match) => {
|
||||
try {
|
||||
const parsedMatch: unknown = JSON.parse(match);
|
||||
const normalizedMatch = JSON.stringify(parsedMatch);
|
||||
// Remove if it matches our function call when normalized
|
||||
return normalizedMatch === normalizedTarget ? '' : match;
|
||||
} catch {
|
||||
// If it's not valid JSON, keep it
|
||||
return match;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// If function call string isn't valid JSON, we've done what we can
|
||||
}
|
||||
|
||||
// Step 4: Clean up multiple consecutive whitespace/newlines left by removals
|
||||
sanitized = sanitized.replace(/\n{3,}/g, '\n\n').trim();
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private requestWithContext(request: string) {
|
||||
const activeFile = this.workSpaceService.getActiveFile();
|
||||
return activeFile ? `${request}\nUser current active file: "${activeFile.path}"` : request;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AIProvider, fromModel } from "Enums/ApiProvider";
|
|||
import type VaultkeeperAIPlugin from "main";
|
||||
import { RegisterSingleton, RegisterTransient, Resolve } from "./DependencyService";
|
||||
import { Services } from "./Services";
|
||||
import { AIPrompt, type IPrompt } from "AIClasses/IPrompt";
|
||||
import { AIPrompt, type IPrompt } from "AIPrompts/IPrompt";
|
||||
import type { IAIClass } from "AIClasses/IAIClass";
|
||||
import type { IConversationNamingService } from "AIClasses/IConversationNamingService";
|
||||
import { Gemini } from "AIClasses/Gemini/Gemini";
|
||||
|
|
@ -13,7 +13,6 @@ import { ConversationFileSystemService } from "./ConversationFileSystemService";
|
|||
import { ConversationHistoryModal } from "Modals/ConversationHistoryModal";
|
||||
import { AIFunctionService } from "./AIFunctionService";
|
||||
import { StreamingService } from "./StreamingService";
|
||||
import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
|
||||
import { WorkSpaceService } from "./WorkSpaceService";
|
||||
import { ChatService } from "./ChatService";
|
||||
import { ConversationNamingService } from "./ConversationNamingService";
|
||||
|
|
@ -37,6 +36,7 @@ import type { IAIFileService } from "AIClasses/IAIFileService";
|
|||
import { ClaudeFileService } from "AIClasses/Claude/ClaudeFileService";
|
||||
import { GeminiFileService } from "AIClasses/Gemini/GeminiFileService";
|
||||
import { OpenAIFileService } from "AIClasses/OpenAI/OpenAIFileService";
|
||||
import { AIControllerService } from "./AIControllerService";
|
||||
|
||||
export async function RegisterPlugin(plugin: VaultkeeperAIPlugin) {
|
||||
RegisterSingleton<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin, plugin);
|
||||
|
|
@ -59,8 +59,8 @@ export function RegisterDependencies() {
|
|||
RegisterSingleton<ConversationNamingService>(Services.ConversationNamingService, new ConversationNamingService());
|
||||
|
||||
RegisterSingleton<IPrompt>(Services.IPrompt, new AIPrompt());
|
||||
RegisterSingleton<AIFunctionDefinitions>(Services.AIFunctionDefinitions, new AIFunctionDefinitions());
|
||||
RegisterSingleton<AIFunctionService>(Services.AIFunctionService, new AIFunctionService());
|
||||
RegisterSingleton<AIControllerService>(Services.AIControllerService, new AIControllerService());
|
||||
RegisterSingleton<StreamingService>(Services.StreamingService, new StreamingService());
|
||||
RegisterSingleton<ChatService>(Services.ChatService, new ChatService());
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ export function RegisterAiProvider() {
|
|||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new OpenAIConversationNamingService());
|
||||
}
|
||||
|
||||
Resolve<ChatService>(Services.ChatService).resolveAIProvider();
|
||||
Resolve<AIControllerService>(Services.AIControllerService).resolveAIProvider();
|
||||
Resolve<ConversationNamingService>(Services.ConversationNamingService).resolveNamingProvider();
|
||||
Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService).resolveAIFileService();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export class Services {
|
|||
static StreamingService = Symbol("StreamingService");
|
||||
static MarkdownService = Symbol("MarkdownService");
|
||||
static StreamingMarkdownService = Symbol("StreamingMarkdownService");
|
||||
static AIFunctionDefinitions = Symbol("AIFunctionDefinitions");
|
||||
static AIFunctionService = Symbol("AIFunctionService");
|
||||
static AIControllerService = Symbol("AIControllerService");
|
||||
static ChatService = Symbol("ChatService");
|
||||
static SanitiserService = Symbol("SanitiserService");
|
||||
static InputService = Symbol("InputService");
|
||||
|
|
|
|||
109
Types/ExecutionPlan.ts
Normal file
109
Types/ExecutionPlan.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { ExecutionStatus } from "Enums/ExecutionStatus";
|
||||
import { ExecutionStep } from "./ExecutionStep";
|
||||
import type { SubmitPlanArgs } from "AIClasses/Schemas/AIFunctionSchemas";
|
||||
import { Copy, replaceCopy } from "Enums/Copy";
|
||||
|
||||
export class ExecutionPlan {
|
||||
|
||||
private executionSteps: ExecutionStep[] = [];
|
||||
|
||||
public constructor(plan: SubmitPlanArgs) {
|
||||
for (const [index, step] of plan.steps.entries()) {
|
||||
this.executionSteps.push(new ExecutionStep(
|
||||
index,
|
||||
step.description,
|
||||
step.instruction,
|
||||
step.context
|
||||
));
|
||||
}
|
||||
if (this.executionSteps[0]) { // Mark first step as active
|
||||
this.executionSteps[0].status = ExecutionStatus.Active;
|
||||
}
|
||||
}
|
||||
|
||||
public completed(): boolean {
|
||||
return this.executionSteps.every(step => step.status === ExecutionStatus.Completed);
|
||||
}
|
||||
|
||||
public getStatusSummary(): { completed: string[], remaining: string[] } {
|
||||
const completed: string[] = [];
|
||||
const remaining: string[] = [];
|
||||
|
||||
for (const step of this.executionSteps) {
|
||||
const stepDescription = `${step.step + 1}. ${step.description}`;
|
||||
if (step.status === ExecutionStatus.Completed) {
|
||||
completed.push(stepDescription);
|
||||
} else {
|
||||
remaining.push(stepDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return { completed, remaining };
|
||||
}
|
||||
|
||||
public completeExecutionStep(stepNumber: number): object {
|
||||
const stepIndex = stepNumber - 1;
|
||||
|
||||
const currentStep = this.executionSteps[stepIndex];
|
||||
if (!currentStep) {
|
||||
return {
|
||||
error: replaceCopy(Copy.StepDoesNotExistError, [
|
||||
stepNumber.toString(),
|
||||
this.executionSteps.length.toString()
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < stepIndex; i++) { // Ensure all previous steps are completed
|
||||
if (this.executionSteps[i].status !== ExecutionStatus.Completed) {
|
||||
return {
|
||||
error: replaceCopy(Copy.StepMustBeCompletedInOrderError, [
|
||||
stepNumber.toString(),
|
||||
`${i + 1}. ${this.executionSteps[i].description}`
|
||||
])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
currentStep.status = ExecutionStatus.Completed;
|
||||
|
||||
const nextStep = this.executionSteps[stepIndex + 1];
|
||||
if (nextStep) {
|
||||
nextStep.status = ExecutionStatus.Active;
|
||||
return {
|
||||
message: replaceCopy(Copy.StepCompletedWithNextStep, [
|
||||
stepNumber.toString(),
|
||||
(stepNumber + 1).toString(),
|
||||
nextStep.description
|
||||
])
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: replaceCopy(Copy.AllStepsCompleted, [
|
||||
stepNumber.toString()
|
||||
])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public toFunctionResponse(): object {
|
||||
if (this.executionSteps.length === 0) {
|
||||
return {
|
||||
error: Copy.PlanningFailedError
|
||||
};
|
||||
}
|
||||
|
||||
const firstStep = this.executionSteps[0];
|
||||
|
||||
return {
|
||||
plan: this.executionSteps.map((step, index) => `${index + 1}. ${step.description}`),
|
||||
firstStep: {
|
||||
step: firstStep.step + 1,
|
||||
description: firstStep.description,
|
||||
instruction: firstStep.instruction,
|
||||
...(firstStep.context && { context: firstStep.context })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
19
Types/ExecutionStep.ts
Normal file
19
Types/ExecutionStep.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { ExecutionStatus } from "Enums/ExecutionStatus";
|
||||
|
||||
export class ExecutionStep {
|
||||
|
||||
public step: number;
|
||||
public status: ExecutionStatus;
|
||||
public description: string;
|
||||
public instruction: string;
|
||||
public context?: string;
|
||||
|
||||
public constructor(step: number, description: string, instruction: string, context?: string) {
|
||||
this.step = step;
|
||||
this.description = description;
|
||||
this.instruction = instruction;
|
||||
this.context = context;
|
||||
this.status = ExecutionStatus.Pending;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue