Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class ThinkConsumer implements AutoCloseable {

Be concise and helpful. When using tools, explain what you're doing and why.""";

private static final String SYSTEM_PROMPT_TEMPLATE = loadSystemPromptTemplate() + "\n\n%s";
private static final String SYSTEM_PROMPT_BASE = loadSystemPromptTemplate();

private static String loadSystemPromptTemplate() {
return loadSystemPromptFromFile(AppConfig.SYSTEM_PROMPT_FILE);
Expand Down Expand Up @@ -353,15 +353,19 @@ private void produceResponse(String sessionId, ThinkResponse response) throws Ex
* Builds the system prompt, injecting memoir context if available.
*/
static String buildSystemPrompt(String memoirContext) {
StringBuilder extra = new StringBuilder();
return buildSystemPrompt(SYSTEM_PROMPT_BASE, memoirContext);
}

static String buildSystemPrompt(String basePrompt, String memoirContext) {
StringBuilder out = new StringBuilder(basePrompt).append("\n\n");

if (memoirContext != null && !memoirContext.isBlank()) {
extra.append("\n\nUser memoir (known facts about this user from previous sessions):\n");
extra.append(memoirContext);
extra.append("\n\nUse the memoir to personalize your responses.");
out.append("\n\nUser memoir (known facts about this user from previous sessions):\n");
out.append(memoirContext);
out.append("\n\nUse the memoir to personalize your responses.");
}

return String.format(SYSTEM_PROMPT_TEMPLATE, extra.toString());
return out.toString();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,34 @@ void missingFile_throws() {
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("SYSTEM_PROMPT_FILE not found");
}

@Test
@DisplayName("System prompt containing special characters is preserved literally (issue #23)")
void specialChars_inPrompt_doesNotThrow() {
String basePrompt = "Apply a 10% discount when asked.";

String withoutMemoir = ThinkConsumer.buildSystemPrompt(basePrompt, null);
assertThat(withoutMemoir).contains("10% discount");

String withMemoir = ThinkConsumer.buildSystemPrompt(basePrompt, "user likes 50% off coupons");
assertThat(withMemoir)
.contains("10% discount")
.contains("50% off coupons");

// Sequences that String.format would interpret as conversions/specifiers
// must survive unchanged now that the prompt is concatenated, not formatted.
assertThat(ThinkConsumer.buildSystemPrompt("Print %s and %d and %n literally.", null))
.contains("%s and %d and %n literally.");
assertThat(ThinkConsumer.buildSystemPrompt("A 100%% bonus", "memoir with %s token"))
.contains("100%% bonus")
.contains("memoir with %s token");

// Other characters that are easy to mishandle in string plumbing.
assertThat(ThinkConsumer.buildSystemPrompt("Use a backslash \\ and a dollar $ sign.", null))
.contains("backslash \\ and a dollar $ sign.");
assertThat(ThinkConsumer.buildSystemPrompt("Braces {0} {1} and brackets [x] stay.", null))
.contains("Braces {0} {1} and brackets [x] stay.");
assertThat(ThinkConsumer.buildSystemPrompt("Quotes \"q\" 'a' and emoji 🚀 stay.", null))
.contains("Quotes \"q\" 'a' and emoji 🚀 stay.");
}
}
Loading