diff --git a/core/config/load.ts b/core/config/load.ts index 1d29124d6e..023439dc1d 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -369,7 +369,7 @@ async function intermediateToFinalConfig( ideSettings, writeLog, config.completionOptions, - config.systemMessage, + config.systemMessage ); if (llm?.providerName === "free-trial") { @@ -397,8 +397,8 @@ async function intermediateToFinalConfig( (config.contextProviders || []) .filter(isContextProviderWithParams) .find((cp) => cp.name === "codebase") as - | ContextProviderWithParams - | undefined + | ContextProviderWithParams + | undefined )?.params || {}; const DEFAULT_CONTEXT_PROVIDERS = [ @@ -991,5 +991,6 @@ export { finalToBrowserConfig, intermediateToFinalConfig, loadContinueConfigFromJson, - type BrowserSerializedContinueConfig, + type BrowserSerializedContinueConfig }; + diff --git a/core/config/types.ts b/core/config/types.ts index 2a68c4c358..947dc0f6db 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -1047,6 +1047,7 @@ declare global { useChromiumForDocsCrawling?: boolean; useTools?: boolean; modelContextProtocolServers?: MCPOptions[]; + systemMessageComposition?: "legacy" | "append" | "prepend" | "placeholders"; } interface AnalyticsConfig { diff --git a/core/index.d.ts b/core/index.d.ts index 7cbf0e25bc..d65fffba08 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -43,13 +43,13 @@ export interface IndexingProgressUpdate { desc: string; shouldClearIndexes?: boolean; status: - | "loading" - | "indexing" - | "done" - | "failed" - | "paused" - | "disabled" - | "cancelled"; + | "loading" + | "indexing" + | "done" + | "failed" + | "paused" + | "disabled" + | "cancelled"; debugInfo?: string; } @@ -675,10 +675,10 @@ export interface IDE { getCurrentFile(): Promise< | undefined | { - isUntitled: boolean; - path: string; - contents: string; - } + isUntitled: boolean; + path: string; + contents: string; + } >; getLastFileSaveTimestamp?(): number; @@ -862,11 +862,11 @@ export interface CustomCommand { export interface Prediction { type: "content"; content: - | string - | { - type: "text"; - text: string; - }[]; + | string + | { + type: "text"; + text: string; + }[]; } export interface ToolExtras { @@ -1134,6 +1134,7 @@ export interface ExperimentalConfig { */ useChromiumForDocsCrawling?: boolean; modelContextProtocolServers?: MCPOptions[]; + systemMessageComposition?: "legacy" | "append" | "prepend" | "placeholders"; } export interface AnalyticsConfig { @@ -1204,9 +1205,9 @@ export interface Config { embeddingsProvider?: EmbeddingsProviderDescription | ILLM; /** The model that Continue will use for tab autocompletions. */ tabAutocompleteModel?: - | CustomLLM - | ModelDescription - | (CustomLLM | ModelDescription)[]; + | CustomLLM + | ModelDescription + | (CustomLLM | ModelDescription)[]; /** Options for tab autocomplete */ tabAutocompleteOptions?: Partial; /** UI styles customization */ @@ -1298,9 +1299,9 @@ export type PackageDetailsSuccess = PackageDetails & { export type PackageDocsResult = { packageInfo: ParsedPackageInfo; } & ( - | { error: string; details?: never } - | { details: PackageDetailsSuccess; error?: never } -); + | { error: string; details?: never } + | { details: PackageDetailsSuccess; error?: never } + ); export interface TerminalOptions { reuseTerminal?: boolean; diff --git a/core/llm/constructMessages.ts b/core/llm/constructMessages.ts index 0e4533cebe..91977b1119 100644 --- a/core/llm/constructMessages.ts +++ b/core/llm/constructMessages.ts @@ -1,4 +1,5 @@ import { + BrowserSerializedContinueConfig, ChatHistoryItem, ChatMessage, MessagePart, @@ -11,15 +12,158 @@ import { modelSupportsTools } from "./autodetect"; const TOOL_USE_RULES = `When using tools, follow the following guidelines: - Avoid calling tools unless they are absolutely necessary. For example, if you are asked a simple programming question you do not need web search. As another example, if the user asks you to explain something about code, do not create a new file.`; +const CODE_BLOCK_INSTRUCTIONS = "Always include the language and file name in the info string when you write code blocks, for example '```python file.py'." + +// Helper function to strip whitespace +function stripWhitespace(text: string, stripLeading = false, stripTrailing = false): string { + let result = text; + if (stripLeading) { + result = result.replace(/^[\s\n]+/, ''); + } + if (stripTrailing) { + result = result.replace(/[\s\n]+$/, ''); + } + return result; +} + +// Helper function for placeholder replacement with whitespace handling +function replacePlaceholder( + text: string, + placeholder: string, + replacement: string +): string { + if (!text.includes(placeholder)) { + return text; + } + + const placeholderIndex = text.indexOf(placeholder); + const placeholderLength = placeholder.length; + + // Check surroundings to determine whitespace needs + const isAtBeginning = placeholderIndex === 0; + const isAtEnd = placeholderIndex + placeholderLength === text.length; + + if (!replacement) { + // No replacement to add - just replace with empty string and clean up + let processed = text.replace(placeholder, ""); + return stripWhitespace(processed, isAtBeginning, isAtEnd); + } else { + // Add replacement with appropriate whitespace + let formattedReplacement = replacement; + + if (!isAtBeginning && !(/[\n\s]$/.test(text.substring(0, placeholderIndex)))) { + formattedReplacement = "\n\n" + formattedReplacement; + }; + + if (!isAtEnd && !(/^[\n\s]/.test(text.substring(placeholderIndex + placeholderLength)))) { + formattedReplacement = formattedReplacement + "\n\n"; + }; + + return text.replace(placeholder, formattedReplacement); + } +} + function constructSystemPrompt( modelDescription: ModelDescription, useTools: boolean, + continueConfig: BrowserSerializedContinueConfig ): string | null { - let systemMessage = - "Always include the language and file name in the info string when you write code blocks, for example '```python file.py'."; - if (useTools && modelSupportsTools(modelDescription)) { - systemMessage += "\n\n" + TOOL_USE_RULES; + + let systemMessage = ""; + // We we have no access to the LLM class, we final systemMessage have to be the same as in core/llm/index.ts + const userSystemMessage = modelDescription.systemMessage ?? continueConfig.systemMessage; + + // Get templates from model description or use defaults + const codeBlockTemplate = modelDescription.promptTemplates?.codeBlockInstructions ?? CODE_BLOCK_INSTRUCTIONS; + + const toolUseTemplate = modelDescription.promptTemplates?.toolUseRules ?? TOOL_USE_RULES; + + // Determine which instructions to include + const codeBlockInstructions = codeBlockTemplate; + const toolUseInstructions = useTools && modelSupportsTools(modelDescription) ? toolUseTemplate : ""; + + switch ((continueConfig.experimental?.systemMessageComposition || "legacy")) { + case "prepend": + // Put user system message first, then default instructions + systemMessage = userSystemMessage || ""; + + if (systemMessage && codeBlockInstructions) { + systemMessage += "\n\n" + codeBlockInstructions; + } else if (codeBlockInstructions) { + systemMessage = codeBlockInstructions; + } + + if (systemMessage && toolUseInstructions) { + systemMessage += "\n\n" + toolUseInstructions; + } else if (toolUseInstructions) { + systemMessage = toolUseInstructions; + } + break; + + case "placeholders": + case "placeholders": + if (userSystemMessage) { + // Define placeholders + const allDefaultInstructions = [ + codeBlockInstructions, + toolUseInstructions + ].filter(Boolean).join("\n\n"); + + // Replace placeholders in user system message + let processedMessage = userSystemMessage; + + // Replace all placeholders using our helper function + processedMessage = replacePlaceholder( + processedMessage, + "{DEFAULT_INSTRUCTIONS}", + allDefaultInstructions + ); + + processedMessage = replacePlaceholder( + processedMessage, + "{CODE_BLOCK_INSTRUCTIONS}", + codeBlockInstructions + ); + + processedMessage = replacePlaceholder( + processedMessage, + "{TOOL_USE_RULES}", + toolUseInstructions + ); + + systemMessage = processedMessage; + } else { + // Fall back to legacy behavior if no user system message + systemMessage = codeBlockInstructions; + if (toolUseInstructions) { + systemMessage += "\n\n" + toolUseInstructions; + } + } + break; + + + case "legacy": + case "append": + default: + systemMessage = codeBlockInstructions; + if (useTools && modelSupportsTools(modelDescription)) { + systemMessage += "\n\n" + toolUseTemplate; + } + // logic moved from core/llm/countTokens.ts + if (userSystemMessage && userSystemMessage.trim() !== "") { + const shouldAddNewLines = systemMessage !== ""; + if (shouldAddNewLines) { + systemMessage += "\n\n"; + } + systemMessage += userSystemMessage; + } + if (userSystemMessage === "") { + // Used defined explicit empty system message will be forced + systemMessage = ""; + } + break; } + return systemMessage; } @@ -30,13 +174,14 @@ export function constructMessages( history: ChatHistoryItem[], modelDescription: ModelDescription, useTools: boolean, + continueConfig: BrowserSerializedContinueConfig, ): ChatMessage[] { const filteredHistory = history.filter( (item) => item.message.role !== "system", ); const msgs: ChatMessage[] = []; - const systemMessage = constructSystemPrompt(modelDescription, useTools); + const systemMessage = constructSystemPrompt(modelDescription, useTools, continueConfig); if (systemMessage) { msgs.push({ role: "system", diff --git a/core/llm/countTokens.ts b/core/llm/countTokens.ts index dfbd2da5d7..d2160d12be 100644 --- a/core/llm/countTokens.ts +++ b/core/llm/countTokens.ts @@ -27,9 +27,9 @@ class LlamaEncoding implements Encoding { } class NonWorkerAsyncEncoder implements AsyncEncoder { - constructor(private readonly encoding: Encoding) {} + constructor(private readonly encoding: Encoding) { } - async close(): Promise {} + async close(): Promise { } async encode(text: string): Promise { return this.encoding.encode(text); @@ -383,8 +383,8 @@ function compileChatMessages( ): ChatMessage[] { let msgsCopy = msgs ? msgs - .map((msg) => ({ ...msg })) - .filter((msg) => !chatMessageIsEmpty(msg) && msg.role !== "system") + .map((msg) => ({ ...msg })) + .filter((msg) => !chatMessageIsEmpty(msg) && msg.role !== "system") : []; msgsCopy = addSpaceToAnyEmptyMessages(msgsCopy); @@ -397,6 +397,7 @@ function compileChatMessages( msgsCopy.push(promptMsg); } + /* Original logic, moved to core/llm/constructMessages.ts if ( (systemMessage && systemMessage.trim() !== "") || msgs?.[0]?.role === "system" @@ -419,6 +420,29 @@ function compileChatMessages( // Insert as second to last // Later moved to top, but want second-priority to last user message msgsCopy.splice(-1, 0, systemChatMsg); + }*/ + + if ( + msgs?.[0]?.role === "system" + ) { + let content = ""; + + content = renderChatMessage(msgs?.[0]); + + const systemChatMsg: ChatMessage = { + role: "system", + content, + }; + // Insert as second to last + // Later moved to top, but want second-priority to last user message + msgsCopy.splice(-1, 0, systemChatMsg); + } else if (systemMessage && systemMessage.trim() !== "") { + // In case core/llm/constructMessages.ts constructSystemPrompt() is not called + const systemChatMsg: ChatMessage = { + role: "system", + content: systemMessage, + }; + msgsCopy.splice(-1, 0, systemChatMsg); } let functionTokens = 0; @@ -469,5 +493,6 @@ export { pruneLinesFromTop, pruneRawPromptFromTop, pruneStringFromBottom, - pruneStringFromTop, + pruneStringFromTop }; + diff --git a/core/llm/llms/index.ts b/core/llm/llms/index.ts index 32cda4aec2..c9f50c602b 100644 --- a/core/llm/llms/index.ts +++ b/core/llm/llms/index.ts @@ -3,7 +3,7 @@ import { IdeSettings, ILLM, LLMOptions, - ModelDescription, + ModelDescription } from "../.."; import { renderTemplatedString } from "../../promptFiles/v1/renderTemplatedString"; import { BaseLLM } from "../index"; @@ -19,7 +19,6 @@ import Cohere from "./Cohere"; import DeepInfra from "./DeepInfra"; import Deepseek from "./Deepseek"; import Fireworks from "./Fireworks"; -import NCompass from "./NCompass"; import Flowise from "./Flowise"; import FreeTrial from "./FreeTrial"; import FunctionNetwork from "./FunctionNetwork"; @@ -35,7 +34,9 @@ import Mistral from "./Mistral"; import MockLLM from "./Mock"; import Moonshot from "./Moonshot"; import Msty from "./Msty"; +import NCompass from "./NCompass"; import Nebius from "./Nebius"; +import Novita from "./Novita"; import Nvidia from "./Nvidia"; import Ollama from "./Ollama"; import OpenAI from "./OpenAI"; @@ -49,7 +50,6 @@ import ContinueProxy from "./stubs/ContinueProxy"; import TestLLM from "./Test"; import TextGenWebUI from "./TextGenWebUI"; import Together from "./Together"; -import Novita from "./Novita"; import VertexAI from "./VertexAI"; import Vllm from "./Vllm"; import WatsonX from "./WatsonX"; diff --git a/docs/docs/json-reference.md b/docs/docs/json-reference.md index ba21ccb65e..9511fc0a5b 100644 --- a/docs/docs/json-reference.md +++ b/docs/docs/json-reference.md @@ -405,6 +405,16 @@ Several experimental config parameters are available, as described below: - `fix`: Prompt for fixing code. - `optimize`: Prompt for optimizing code. - `modelContextProtocolServers`: See [Model Context Protocol](/customize/context-providers#model-context-protocol) +- `systemMessageComposition`: Controls how the default system instructions and user-provided system message are combined: + - `legacy` or `append` (default): Default instructions are placed first, followed by user system message. + - `prepend`: User system message is placed first, followed by default instructions. + - `placeholders`: User system message can include special placeholders to control where default instructions appear: + + - `{DEFAULT_INSTRUCTIONS}`: Includes both code block and tool use instructions. + - `{CODE_BLOCK_INSTRUCTIONS}`: Includes only the code block formatting instructions. + - `{TOOL_USE_RULES}`: Includes only the tool use rules (when applicable). + + These placeholders correspond to templates that can be customized in a model's `promptTemplates` object using the keys `codeBlockInstructions` and `toolUseRules`. Example @@ -432,7 +442,8 @@ Example "args": ["mcp-server-sqlite", "--db-path", "/Users/NAME/test.db"] } } - ] + ], + "systemMessageComposition": "placeholders" } } ``` diff --git a/docs/docs/reference.md b/docs/docs/reference.md index 143fec98b0..041a64081e 100644 --- a/docs/docs/reference.md +++ b/docs/docs/reference.md @@ -371,6 +371,16 @@ Several experimental config parameters are available, as described below: - `fix`: Prompt for fixing code. - `optimize`: Prompt for optimizing code. - `modelContextProtocolServers`: See [Model Context Protocol](/customize/context-providers#model-context-protocol) +- `systemMessageComposition`: Controls how the default system instructions and user-provided system message are combined: + - `legacy` or `append` (default): Default instructions are placed first, followed by user system message. + - `prepend`: User system message is placed first, followed by default instructions. + - `placeholders`: User system message can include special placeholders to control where default instructions appear: + + - `{DEFAULT_INSTRUCTIONS}`: Includes both code block and tool use instructions. + - `{CODE_BLOCK_INSTRUCTIONS}`: Includes only the code block formatting instructions. + - `{TOOL_USE_RULES}`: Includes only the tool use rules (when applicable). + + These placeholders correspond to templates that can be customized in a model's `promptTemplates` object using the keys `codeBlockInstructions` and `toolUseRules`. Example @@ -398,7 +408,8 @@ Example "args": ["mcp-server-sqlite", "--db-path", "/Users/NAME/test.db"] } } - ] + ], + "systemMessageComposition": "placeholders" } } ``` diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/reference.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/reference.md index 00e72eb427..dda51dff1d 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/reference.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/reference.md @@ -402,8 +402,6 @@ Several experimental config parameters are available, as described below: - `inlineEdit`: Model title for inline edits. - `applyCodeBlock`: Model title for applying code blocks. - `repoMapFileSelection`: Model title for repo map selections. -- `readResponseTTS`: If `true`, reads LLM responses aloud with TTS. Default is `true`. -- `promptPath`: Change the path to custom prompt files from the default ".prompts" - `quickActions`: Array of custom quick actions - `title` (**required**): Display title for the quick action. - `prompt` (**required**): Prompt for quick action. @@ -413,8 +411,17 @@ Several experimental config parameters are available, as described below: - `docstring`: Prompt for adding docstrings. - `fix`: Prompt for fixing code. - `optimize`: Prompt for optimizing code. - - `fixGrammar`: Prompt for fixing grammar or spelling. -- `useChromiumForDocsCrawling`: Use Chromium to crawl docs locally. Useful if the default Cheerio crawler fails on sites that require JavaScript rendering. Downloads and installs Chromium to `~/.continue/.utils`.. +- `systemMessageComposition`: Controls how the default system instructions and user-provided system message are combined: + + - `legacy` or `append` (default): Default instructions are placed first, followed by user system message. + - `prepend`: User system message is placed first, followed by default instructions. + - `placeholders`: User system message can include special placeholders to control where default instructions appear: + + - `{DEFAULT_INSTRUCTIONS}`: Includes both code block and tool use instructions. + - `{CODE_BLOCK_INSTRUCTIONS}`: Includes only the code block formatting instructions. + - `{TOOL_USE_RULES}`: Includes only the tool use rules (when applicable). + + These placeholders correspond to templates that can be customized in a model's `promptTemplates` object using the keys `codeBlockInstructions` and `toolUseRules`. Example @@ -424,7 +431,6 @@ Example "modelRoles": { "inlineEdit": "Edit Model" }, - "promptPath": "custom/.prompts", "quickActions": [ { "title": "Tags", @@ -435,8 +441,16 @@ Example "contextMenuPrompts": { "fixGrammar": "Fix grammar in the above but allow for typos." }, - "readResponseTTS": false, - "useChromiumForDocsCrawling": true + "modelContextProtocolServers": [ + { + "transport": { + "type": "stdio", + "command": "uvx", + "args": ["mcp-server-sqlite", "--db-path", "/Users/NAME/test.db"] + } + } + ], + "systemMessageComposition": "placeholders" } } ``` diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json index 8a1153d0bd..f3de0843e6 100644 --- a/extensions/vscode/config_schema.json +++ b/extensions/vscode/config_schema.json @@ -373,9 +373,25 @@ }, "promptTemplates": { "title": "Prompt Templates", - "markdownDescription": "A mapping of prompt template name ('edit' is currently the only one used in Continue) to a string giving the prompt template. See [here](https://docs.continue.dev/model-setup/configuration#customizing-the-edit-prompt) for an example.", - "x-intellij-html-description": "A mapping of prompt template name ('edit' is currently the only one used in Continue) to a string giving the prompt template. See here for an example.", + "markdownDescription": "A mapping of prompt template names to strings giving the prompt templates. See [here](https://docs.continue.dev/model-setup/configuration#customizing-the-edit-prompt) for examples.", + "x-intellij-html-description": "A mapping of prompt template names to strings giving the prompt templates. See here for examples.", "type": "object", + "properties": { + "edit": { + "type": "string", + "description": "Template used for edit operations in Continue" + }, + "codeBlockInstructions": { + "type": "string", + "description": "Custom instructions for code block formatting (used with system message placeholders)", + "default": "Always include the language and file name in the info string when you write code blocks, for example '```python file.py'." + }, + "toolUseRules": { + "type": "string", + "description": "Custom instructions for tool usage (used with system message placeholders)", + "default": "When using tools, follow the following guidelines:\n- Avoid calling tools unless they are absolutely necessary. For example, if you are asked a simple programming question you do not need web search. As another example, if the user asks you to explain something about code, do not create a new file." + } + }, "additionalProperties": { "type": "string" } @@ -3355,6 +3371,12 @@ }, "required": ["transport"] } + }, + "systemMessageComposition": { + "type": "string", + "enum": ["legacy", "append", "prepend", "placeholders"], + "default": "legacy", + "description": "Controls how the default system instructions and user-provided system message are combined. 'legacy' or 'append': Default instructions first, then user message. 'prepend': User message first, then default instructions. 'placeholders': User message can include special placeholders {DEFAULT_INSTRUCTIONS}, {CODE_BLOCK_INSTRUCTIONS}, and {TOOL_USE_RULES}." } } } diff --git a/gui/src/redux/thunks/streamResponse.ts b/gui/src/redux/thunks/streamResponse.ts index 9c0cb4b9af..90d20103e9 100644 --- a/gui/src/redux/thunks/streamResponse.ts +++ b/gui/src/redux/thunks/streamResponse.ts @@ -34,10 +34,10 @@ const getSlashCommandForInput = ( typeof input === "string" ? input : ( - input.filter((part) => part.type === "text").slice(-1)[0] as - | TextMessagePart - | undefined - )?.text || ""; + input.filter((part) => part.type === "text").slice(-1)[0] as + | TextMessagePart + | undefined + )?.text || ""; if (lastText.startsWith("/")) { slashCommandName = lastText.split(" ")[0].substring(1); @@ -124,6 +124,7 @@ export const streamResponseThunk = createAsyncThunk< [...updatedHistory], defaultModel, useTools, + state.config.config ); posthog.capture("step run", { diff --git a/gui/src/redux/thunks/streamResponseAfterToolCall.ts b/gui/src/redux/thunks/streamResponseAfterToolCall.ts index 3eb11b2494..21f2639cfa 100644 --- a/gui/src/redux/thunks/streamResponseAfterToolCall.ts +++ b/gui/src/redux/thunks/streamResponseAfterToolCall.ts @@ -64,6 +64,7 @@ export const streamResponseAfterToolCall = createAsyncThunk< [...updatedHistory], defaultModel, useTools, + state.config.config ); const output = await dispatch(streamNormalInput(messages)); unwrapResult(output);