feat: custom prompt

This commit is contained in:
duanfuxiang 2025-04-27 13:21:51 +08:00
parent b5a322df31
commit 5558c96aa1
7 changed files with 307 additions and 202 deletions

View File

@ -0,0 +1 @@
export const ROOT_DIR = '_infio_prompts'

View File

@ -1,25 +1,24 @@
import fs from "fs/promises"
import path from "path"
async function safeReadFile(filePath: string): Promise<string> {
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<string> {
const ruleFilesFolder = path.join(ROOT_DIR, `${mode}/rules/`)
if (!(await app.vault.adapter.exists(ruleFilesFolder))) {
return ""
}
}
export async function loadRuleFiles(cwd: string): Promise<string> {
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<string> {
}
export async function addCustomInstructions(
app: App,
modeCustomInstructions: string,
globalCustomInstructions: string,
cwd: string,
mode: string,
options: { preferredLanguage?: string } = {},
): Promise<string> {
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())
}

View File

@ -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<string> {
* 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<string> {
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<void> {
const rooDir = path.join(cwd, ".roo")
export async function ensureInfioPromptsDirectory(cwd: string): Promise<void> {
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<void> {
// 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

View File

@ -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<void> {
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<string> {
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<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> {
// 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<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> {
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,
)
}
}

View File

@ -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<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> {
// 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<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> => {
// 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,
)
}

47
src/utils/fs.ts Normal file
View File

@ -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<string[]> {
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<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}

View File

@ -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<RequestMessage> {
const systemPrompt = await SYSTEM_PROMPT(
const systemPrompt = await this.systemPromptsManager.getSystemPrompt(
this.app.vault.getRoot().path,
false,
mode,