From 5558c96aa1cadfe27f40255634bc13b7be885c96 Mon Sep 17 00:00:00 2001 From: duanfuxiang Date: Sun, 27 Apr 2025 13:21:51 +0800 Subject: [PATCH] feat: custom prompt --- src/core/prompts/constants.ts | 1 + .../prompts/sections/custom-instructions.ts | 47 ++-- .../prompts/sections/custom-system-prompt.ts | 16 +- src/core/prompts/system-prompts-manager.ts | 220 ++++++++++++++++++ src/core/prompts/system.ts | 173 -------------- src/utils/fs.ts | 47 ++++ src/utils/prompt-generator.ts | 5 +- 7 files changed, 307 insertions(+), 202 deletions(-) create mode 100644 src/core/prompts/constants.ts create mode 100644 src/core/prompts/system-prompts-manager.ts delete mode 100644 src/core/prompts/system.ts create mode 100644 src/utils/fs.ts diff --git a/src/core/prompts/constants.ts b/src/core/prompts/constants.ts new file mode 100644 index 0000000..195ac98 --- /dev/null +++ b/src/core/prompts/constants.ts @@ -0,0 +1 @@ +export const ROOT_DIR = '_infio_prompts' diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 240dfcc..af5af27 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -1,25 +1,24 @@ -import fs from "fs/promises" -import path from "path" -async function safeReadFile(filePath: string): Promise { - try { - const content = await fs.readFile(filePath, "utf-8") - return content.trim() - } catch (err) { - const errorCode = (err as NodeJS.ErrnoException).code - if (!errorCode || !["ENOENT", "EISDIR"].includes(errorCode)) { - throw err - } +import * as path from 'path' + +import { App, normalizePath } from 'obsidian' + +import { ROOT_DIR } from '../constants' + +export async function loadRuleFiles( + app: App, + mode: string, +): Promise { + const ruleFilesFolder = path.join(ROOT_DIR, `${mode}/rules/`) + if (!(await app.vault.adapter.exists(ruleFilesFolder))) { return "" } -} -export async function loadRuleFiles(cwd: string): Promise { - const ruleFiles = [".clinerules", ".cursorrules", ".windsurfrules"] + const ruleFiles = await app.vault.adapter.list(normalizePath(ruleFilesFolder)) + let combinedRules = "" - - for (const file of ruleFiles) { - const content = await safeReadFile(path.join(cwd, file)) + for (const file of ruleFiles.files) { + const content = await app.vault.adapter.read(normalizePath(file)) if (content) { combinedRules += `\n# Rules from ${file}:\n${content}\n` } @@ -29,19 +28,23 @@ export async function loadRuleFiles(cwd: string): Promise { } export async function addCustomInstructions( + app: App, modeCustomInstructions: string, globalCustomInstructions: string, cwd: string, mode: string, options: { preferredLanguage?: string } = {}, ): Promise { + console.log("addCustomInstructions this app, ", app) const sections = [] - // Load mode-specific rules if mode is provided + // Load mode-specific rules file if mode is provided let modeRuleContent = "" if (mode) { - const modeRuleFile = `.clinerules-${mode}` - modeRuleContent = await safeReadFile(path.join(cwd, modeRuleFile)) + const modeRulesFile = path.join(ROOT_DIR, `${mode}/rules.md`) + if (await app.vault.adapter.exists(modeRulesFile)) { + modeRuleContent = await app.vault.adapter.read(normalizePath(modeRulesFile)) + } } // Add language preference if provided @@ -66,12 +69,12 @@ export async function addCustomInstructions( // Add mode-specific rules first if they exist if (modeRuleContent && modeRuleContent.trim()) { - const modeRuleFile = `.clinerules-${mode}` + const modeRuleFile = `${mode}_rules.md` rules.push(`# Rules from ${modeRuleFile}:\n${modeRuleContent}`) } // Add generic rules - const genericRuleContent = await loadRuleFiles(cwd) + const genericRuleContent = await loadRuleFiles(app, mode) if (genericRuleContent && genericRuleContent.trim()) { rules.push(genericRuleContent.trim()) } diff --git a/src/core/prompts/sections/custom-system-prompt.ts b/src/core/prompts/sections/custom-system-prompt.ts index 44c7d25..0f2935b 100644 --- a/src/core/prompts/sections/custom-system-prompt.ts +++ b/src/core/prompts/sections/custom-system-prompt.ts @@ -5,6 +5,7 @@ import path from "path" import { Mode } from "../../../shared/modes" import { fileExistsAtPath } from "../../../utils/fs" +import { Mode } from "../../../utils/modes" /** * Safely reads a file, returning an empty string if the file doesn't exist @@ -27,23 +28,26 @@ async function safeReadFile(filePath: string): Promise { * Get the path to a system prompt file for a specific mode */ export function getSystemPromptFilePath(cwd: string, mode: Mode): string { - return path.join(cwd, ".roo", `system-prompt-${mode}`) + return path.join(cwd, "_infio_prompts", `${mode}_system_prompt`) } /** - * Loads custom system prompt from a file at .roo/system-prompt-[mode slug] + * Loads custom system prompt from a file at _infio_prompts/system-prompt-[mode slug] * If the file doesn't exist, returns an empty string */ export async function loadSystemPromptFile(cwd: string, mode: Mode): Promise { + console.log("cwd", cwd) + console.log("mode", mode) const filePath = getSystemPromptFilePath(cwd, mode) + console.log("filePath", filePath) return safeReadFile(filePath) } /** - * Ensures the .roo directory exists, creating it if necessary + * Ensures the _infio_prompts directory exists, creating it if necessary */ -export async function ensureRooDirectory(cwd: string): Promise { - const rooDir = path.join(cwd, ".roo") +export async function ensureInfioPromptsDirectory(cwd: string): Promise { + const infioPromptsDir = path.join(cwd, "_infio_prompts") // Check if directory already exists if (await fileExistsAtPath(rooDir)) { @@ -52,7 +56,7 @@ export async function ensureRooDirectory(cwd: string): Promise { // Create the directory try { - await fs.mkdir(rooDir, { recursive: true }) + await fs.mkdir(infioPromptsDir, { recursive: true }) } catch (err) { // If directory already exists (race condition), ignore the error const errorCode = (err as NodeJS.ErrnoException).code diff --git a/src/core/prompts/system-prompts-manager.ts b/src/core/prompts/system-prompts-manager.ts new file mode 100644 index 0000000..f94146a --- /dev/null +++ b/src/core/prompts/system-prompts-manager.ts @@ -0,0 +1,220 @@ + +import * as path from 'path' + +import { App, normalizePath } from 'obsidian' + +import { + CustomModePrompts, + Mode, + ModeConfig, + PromptComponent, + defaultModeSlug, + getGroupName, + getModeBySlug, + modes +} from "../../utils/modes" +import { DiffStrategy } from "../diff/DiffStrategy" +import { McpHub } from "../mcp/McpHub" + + +import { ROOT_DIR } from './constants' +import { + addCustomInstructions, + getCapabilitiesSection, + getMcpServersSection, + getModesSection, + getObjectiveSection, + getRulesSection, + getSharedToolUseSection, + getSystemInfoSection, + getToolUseGuidelinesSection, +} from "./sections" +// import { loadSystemPromptFile } from "./sections/custom-system-prompt" +import { getToolDescriptionsForMode } from "./tools" + + +export class SystemPromptsManager { + protected dataDir: string + protected app: App + + constructor(app: App) { + this.app = app + this.dataDir = normalizePath(`${ROOT_DIR}`) + this.ensureDirectory() + } + + private async ensureDirectory(): Promise { + console.log("this.app, ", this.app) + if (!(await this.app.vault.adapter.exists(this.dataDir))) { + await this.app.vault.adapter.mkdir(this.dataDir) + } + } + + private getSystemPromptFilePath(mode: Mode): string { + // Format: {mode slug}_system_prompt.md + return `${mode}/system_prompt.md` + } + + private async loadSystemPromptFile(mode: Mode): Promise { + const fileName = this.getSystemPromptFilePath(mode) + const filePath = normalizePath(path.join(this.dataDir, fileName)) + if (!(await this.app.vault.adapter.exists(filePath))) { + return "" + } + const content = await this.app.vault.adapter.read(filePath) + return content + } + + private async generatePrompt( + cwd: string, + supportsComputerUse: boolean, + mode: Mode, + filesSearchMethod: string, + mcpHub?: McpHub, + diffStrategy?: DiffStrategy, + browserViewportSize?: string, + promptComponent?: PromptComponent, + customModeConfigs?: ModeConfig[], + globalCustomInstructions?: string, + preferredLanguage?: string, + diffEnabled?: boolean, + experiments?: Record, + enableMcpServerCreation?: boolean, + ): Promise { + // if (!context) { + // throw new Error("Extension context is required for generating system prompt") + // } + + // // If diff is disabled, don't pass the diffStrategy + // const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined + + // Get the full mode config to ensure we have the role definition + const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] + const roleDefinition = promptComponent?.roleDefinition || modeConfig.roleDefinition + + const [modesSection, mcpServersSection] = await Promise.all([ + getModesSection(), + modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") + ? getMcpServersSection(mcpHub, diffStrategy, enableMcpServerCreation) + : Promise.resolve(""), + ]) + + const basePrompt = `${roleDefinition} + +${getSharedToolUseSection()} + +${getToolDescriptionsForMode( + mode, + cwd, + filesSearchMethod, + supportsComputerUse, + diffStrategy, + browserViewportSize, + mcpHub, + customModeConfigs, + experiments, + )} + +${getToolUseGuidelinesSection()} + +${mcpServersSection} + +${getCapabilitiesSection( + mode, + cwd, + filesSearchMethod, + )} + +${modesSection} + +${getRulesSection( + mode, + cwd, + filesSearchMethod, + supportsComputerUse, + diffStrategy, + experiments, + )} + +${getSystemInfoSection(cwd)} + +${getObjectiveSection(mode)} + +${await addCustomInstructions(this.app, promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}` + + return basePrompt + } + + public async getSystemPrompt( + cwd: string, + supportsComputerUse: boolean, + mode: Mode = defaultModeSlug, + filesSearchMethod: string = 'regex', + preferredLanguage?: string, + diffStrategy?: DiffStrategy, + mcpHub?: McpHub, + browserViewportSize?: string, + customModePrompts?: CustomModePrompts, + customModes?: ModeConfig[], + globalCustomInstructions?: string, + diffEnabled?: boolean, + experiments?: Record, + enableMcpServerCreation?: boolean, + ): Promise { + + const getPromptComponent = (value: unknown): PromptComponent | undefined => { + if (typeof value === "object" && value !== null) { + return value as PromptComponent + } + return undefined + } + + // Try to load custom system prompt from file + const fileCustomSystemPrompt = await this.loadSystemPromptFile(mode) + + // Check if it's a custom mode + const promptComponent = getPromptComponent(customModePrompts?.[mode]) + + // Get full mode config from custom modes or fall back to built-in modes + const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0] + + // If a file-based custom system prompt exists, use it + if (fileCustomSystemPrompt) { + const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition + const customInstructions = await addCustomInstructions( + this.app, + promptComponent?.customInstructions || currentMode.customInstructions || "", + globalCustomInstructions || "", + cwd, + mode, + { preferredLanguage }, + ) + return `${roleDefinition} + +${fileCustomSystemPrompt} + +${customInstructions}` + } + + // // If diff is disabled, don't pass the diffStrategy + // const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined + + return this.generatePrompt( + // context, + cwd, + supportsComputerUse, + currentMode.slug, + filesSearchMethod, + mcpHub, + diffStrategy, + browserViewportSize, + promptComponent, + customModes, + globalCustomInstructions, + preferredLanguage, + diffEnabled, + experiments, + enableMcpServerCreation, + ) + } +} diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts deleted file mode 100644 index 212bead..0000000 --- a/src/core/prompts/system.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { - CustomModePrompts, - Mode, - ModeConfig, - PromptComponent, - defaultModeSlug, - getGroupName, - getModeBySlug, - modes -} from "../../utils/modes" -import { DiffStrategy } from "../diff/DiffStrategy" -import { McpHub } from "../mcp/McpHub" - -import { - addCustomInstructions, - getCapabilitiesSection, - getMcpServersSection, - getModesSection, - getObjectiveSection, - getRulesSection, - getSharedToolUseSection, - getSystemInfoSection, - getToolUseGuidelinesSection, -} from "./sections" -import { getToolDescriptionsForMode } from "./tools" - -async function generatePrompt( - cwd: string, - supportsComputerUse: boolean, - mode: Mode, - filesSearchMethod: string, - mcpHub?: McpHub, - diffStrategy?: DiffStrategy, - browserViewportSize?: string, - promptComponent?: PromptComponent, - customModeConfigs?: ModeConfig[], - globalCustomInstructions?: string, - preferredLanguage?: string, - diffEnabled?: boolean, - experiments?: Record, - enableMcpServerCreation?: boolean, -): Promise { - // if (!context) { - // throw new Error("Extension context is required for generating system prompt") - // } - - // // If diff is disabled, don't pass the diffStrategy - // const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined - - // Get the full mode config to ensure we have the role definition - const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] - const roleDefinition = promptComponent?.roleDefinition || modeConfig.roleDefinition - - const [modesSection, mcpServersSection] = await Promise.all([ - getModesSection(), - modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") - ? getMcpServersSection(mcpHub, diffStrategy, enableMcpServerCreation) - : Promise.resolve(""), - ]) - - const basePrompt = `${roleDefinition} - -${getSharedToolUseSection()} - -${getToolDescriptionsForMode( - mode, - cwd, - filesSearchMethod, - supportsComputerUse, - diffStrategy, - browserViewportSize, - mcpHub, - customModeConfigs, - experiments, - )} - -${getToolUseGuidelinesSection()} - -${mcpServersSection} - -${getCapabilitiesSection( - mode, - cwd, - filesSearchMethod, - )} - -${modesSection} - -${getRulesSection( - mode, - cwd, - filesSearchMethod, - supportsComputerUse, - diffStrategy, - experiments, - )} - -${getSystemInfoSection(cwd)} - -${getObjectiveSection(mode)} - -${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}` - - return basePrompt -} - -export const SYSTEM_PROMPT = async ( - cwd: string, - supportsComputerUse: boolean, - mode: Mode = defaultModeSlug, - filesSearchMethod: string = 'regex', - preferredLanguage?: string, - diffStrategy?: DiffStrategy, - mcpHub?: McpHub, - browserViewportSize?: string, - customModePrompts?: CustomModePrompts, - customModes?: ModeConfig[], - globalCustomInstructions?: string, - diffEnabled?: boolean, - experiments?: Record, - enableMcpServerCreation?: boolean, -): Promise => { - // if (!context) { - // throw new Error("Extension context is required for generating system prompt") - // } - - const getPromptComponent = (value: unknown) => { - if (typeof value === "object" && value !== null) { - return value as PromptComponent - } - return undefined - } - - // Try to load custom system prompt from file - // const fileCustomSystemPrompt = await loadSystemPromptFile(cwd, mode) - - // Check if it's a custom mode - const promptComponent = getPromptComponent(customModePrompts?.[mode]) - - // Get full mode config from custom modes or fall back to built-in modes - const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0] - - // If a file-based custom system prompt exists, use it - // if (fileCustomSystemPrompt) { - // const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition - // return `${roleDefinition} - - // ${fileCustomSystemPrompt} - - // ${await addCustomInstructions(promptComponent?.customInstructions || currentMode.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}` - // } - - // // If diff is disabled, don't pass the diffStrategy - // const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined - - return generatePrompt( - // context, - cwd, - supportsComputerUse, - currentMode.slug, - filesSearchMethod, - mcpHub, - diffStrategy, - browserViewportSize, - promptComponent, - customModes, - globalCustomInstructions, - preferredLanguage, - diffEnabled, - experiments, - enableMcpServerCreation, - ) -} diff --git a/src/utils/fs.ts b/src/utils/fs.ts new file mode 100644 index 0000000..9f7af84 --- /dev/null +++ b/src/utils/fs.ts @@ -0,0 +1,47 @@ +import fs from "fs/promises" +import * as path from "path" + +/** + * Asynchronously creates all non-existing subdirectories for a given file path + * and collects them in an array for later deletion. + * + * @param filePath - The full path to a file. + * @returns A promise that resolves to an array of newly created directories. + */ +export async function createDirectoriesForFile(filePath: string): Promise { + const newDirectories: string[] = [] + const normalizedFilePath = path.normalize(filePath) // Normalize path for cross-platform compatibility + const directoryPath = path.dirname(normalizedFilePath) + + let currentPath = directoryPath + const dirsToCreate: string[] = [] + + // Traverse up the directory tree and collect missing directories + while (!(await fileExistsAtPath(currentPath))) { + dirsToCreate.push(currentPath) + currentPath = path.dirname(currentPath) + } + + // Create directories from the topmost missing one down to the target directory + for (let i = dirsToCreate.length - 1; i >= 0; i--) { + await fs.mkdir(dirsToCreate[i]) + newDirectories.push(dirsToCreate[i]) + } + + return newDirectories +} + +/** + * Helper function to check if a path exists. + * + * @param path - The path to check. + * @returns A promise that resolves to true if the path exists, false otherwise. + */ +export async function fileExistsAtPath(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} diff --git a/src/utils/prompt-generator.ts b/src/utils/prompt-generator.ts index 959c760..495c5a4 100644 --- a/src/utils/prompt-generator.ts +++ b/src/utils/prompt-generator.ts @@ -8,6 +8,7 @@ import { RAGEngine } from '../core/rag/rag-engine' import { SelectVector } from '../database/schema' import { ChatMessage, ChatUserMessage } from '../types/chat' import { ContentPart, RequestMessage } from '../types/llm/request' +import { SystemPromptsManager } from '../core/prompts/system-prompts-manager' import { MentionableBlock, MentionableFile, @@ -115,6 +116,7 @@ export class PromptGenerator { private app: App private settings: InfioSettings private diffStrategy: DiffStrategy + private systemPromptsManager: SystemPromptsManager private static readonly EMPTY_ASSISTANT_MESSAGE: RequestMessage = { role: 'assistant', content: '', @@ -130,6 +132,7 @@ export class PromptGenerator { this.app = app this.settings = settings this.diffStrategy = diffStrategy + this.systemPromptsManager = new SystemPromptsManager(this.app) } public async generateRequestMessages({ @@ -465,7 +468,7 @@ export class PromptGenerator { } private async getSystemMessageNew(mode: Mode, filesSearchMethod: string, preferredLanguage: string): Promise { - const systemPrompt = await SYSTEM_PROMPT( + const systemPrompt = await this.systemPromptsManager.getSystemPrompt( this.app.vault.getRoot().path, false, mode,