Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

systemMessageComposition experimental configuration for more control over constructed system message #4507

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions core/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ async function intermediateToFinalConfig(
ideSettings,
writeLog,
config.completionOptions,
config.systemMessage,
config.systemMessage
);

if (llm?.providerName === "free-trial") {
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -991,5 +991,6 @@ export {
finalToBrowserConfig,
intermediateToFinalConfig,
loadContinueConfigFromJson,
type BrowserSerializedContinueConfig,
type BrowserSerializedContinueConfig
};

1 change: 1 addition & 0 deletions core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,7 @@ declare global {
useChromiumForDocsCrawling?: boolean;
useTools?: boolean;
modelContextProtocolServers?: MCPOptions[];
systemMessageComposition?: "legacy" | "append" | "prepend" | "placeholders";
}

interface AnalyticsConfig {
Expand Down
45 changes: 23 additions & 22 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -675,10 +675,10 @@ export interface IDE {
getCurrentFile(): Promise<
| undefined
| {
isUntitled: boolean;
path: string;
contents: string;
}
isUntitled: boolean;
path: string;
contents: string;
}
>;

getLastFileSaveTimestamp?(): number;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1134,6 +1134,7 @@ export interface ExperimentalConfig {
*/
useChromiumForDocsCrawling?: boolean;
modelContextProtocolServers?: MCPOptions[];
systemMessageComposition?: "legacy" | "append" | "prepend" | "placeholders";
}

export interface AnalyticsConfig {
Expand Down Expand Up @@ -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<TabAutocompleteOptions>;
/** UI styles customization */
Expand Down Expand Up @@ -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;
Expand Down
155 changes: 150 additions & 5 deletions core/llm/constructMessages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BrowserSerializedContinueConfig,
ChatHistoryItem,
ChatMessage,
MessagePart,
Expand All @@ -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;
}

Expand All @@ -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",
Expand Down
35 changes: 30 additions & 5 deletions core/llm/countTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {}
async close(): Promise<void> { }

async encode(text: string): Promise<number[]> {
return this.encoding.encode(text);
Expand Down Expand Up @@ -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);
Expand All @@ -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"
Expand All @@ -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;
Expand Down Expand Up @@ -469,5 +493,6 @@ export {
pruneLinesFromTop,
pruneRawPromptFromTop,
pruneStringFromBottom,
pruneStringFromTop,
pruneStringFromTop
};

Loading
Loading