diff --git a/src/utils/parseTextTemplate.ts b/src/utils/parseTextTemplate.ts index f370dad7..57d9fd60 100644 --- a/src/utils/parseTextTemplate.ts +++ b/src/utils/parseTextTemplate.ts @@ -67,6 +67,15 @@ export function parseTextTemplate part.text === textPart.separator); + if (matchingPartIndex < partIndex) { + const matchingPart = parts[matchingPartIndex]!; + if (result[matchingPart.key as Parts[number]["key"]] != null) + throw new Error(`Template must contain exactly one "${matchingPart.text}"`); + + throw new Error(`Template must contain "${matchingPart.text}" before "${parts[partIndex - 1]!.text}"`); + } + for (; partIndex < parts.length; partIndex++) { const part = parts[partIndex]!; diff --git a/test/standalone/chatWrappers/generic/TemplateChatWrapper.test.ts b/test/standalone/chatWrappers/generic/TemplateChatWrapper.test.ts index b0bc1e0c..069c356d 100644 --- a/test/standalone/chatWrappers/generic/TemplateChatWrapper.test.ts +++ b/test/standalone/chatWrappers/generic/TemplateChatWrapper.test.ts @@ -4,6 +4,42 @@ import {defaultChatSystemPrompt} from "../../../../src/config.js"; describe("TemplateChatWrapper", () => { + test.each([ + {template: "{{history}}model: {{completion}} done {{completion}}", part: "{{completion}}"}, + {template: "{{history}}model: {{completion}} done {{history}}", part: "{{history}}"} + ] as const)("rejects a trailing $part", ({template, part}) => { + expect(() => new TemplateChatWrapper({ + template, + historyTemplate: { + system: "system: {{message}}\n", + user: "user: {{message}}\n", + model: "model: {{message}}\n" + } + })).toThrow(`Template must contain exactly one "${part}"`); + }); + + test("rejects duplicate history message placeholders", () => { + expect(() => new TemplateChatWrapper({ + template: "{{history}}model: {{completion}}\nuser: ", + historyTemplate: { + system: "system: {{message}}\n", + user: "user: {{message}} / {{message}}\n", + model: "model: {{message}}\n" + } + })).toThrow('Template must contain exactly one "{{message}}"'); + }); + + test("rejects a system prompt after completion", () => { + expect(() => new TemplateChatWrapper({ + template: "{{history}}model: {{completion}}\nuser: {{systemPrompt}}", + historyTemplate: { + system: "system: {{message}}\n", + user: "user: {{message}}\n", + model: "model: {{message}}\n" + } + })).toThrow('Template must contain "{{systemPrompt}}" before "{{completion}}"'); + }); + const conversationHistory: ChatHistoryItem[] = [{ type: "system", text: defaultChatSystemPrompt diff --git a/test/standalone/utils/parseTextTemplate.test.ts b/test/standalone/utils/parseTextTemplate.test.ts index 8a39e012..501490e8 100644 --- a/test/standalone/utils/parseTextTemplate.test.ts +++ b/test/standalone/utils/parseTextTemplate.test.ts @@ -4,6 +4,34 @@ import {parseTextTemplate} from "../../../src/utils/parseTextTemplate.js"; describe("utils", () => { describe("parseTextTemplate", () => { + test.each(["{{name}}", "{{thing}}"])("rejects trailing duplicate %s", (part) => { + expect(() => parseTextTemplate( + `Hello, {{name}}! What is the {{thing}}? ${part} ignored text`, + [{key: "name", text: "{{name}}"}, {key: "thing", text: "{{thing}}"}] + )).toThrow(`Template must contain exactly one "${part}"`); + }); + + test("rejects a duplicate in a single-part template", () => { + expect(() => parseTextTemplate( + "{{message}} / {{message}} trailing text", + [{key: "message", text: "{{message}}"}] + )).toThrow('Template must contain exactly one "{{message}}"'); + }); + + test("rejects a duplicate before a trailing optional part", () => { + expect(() => parseTextTemplate( + "{{name}} / {{name}} trailing text", + [{key: "name", text: "{{name}}"}, {key: "thing", text: "{{thing}}", optional: true}] + )).toThrow('Template must contain exactly one "{{name}}"'); + }); + + test("rejects a skipped optional part after later parts", () => { + expect(() => parseTextTemplate( + "{{thing}} / {{name}} trailing text", + [{key: "name", text: "{{name}}", optional: true}, {key: "thing", text: "{{thing}}"}] + )).toThrow('Template must contain "{{name}}" before "{{thing}}"'); + }); + test("normal", async () => { const res = parseTextTemplate( "Hello, {{name}}! What is the {{thing}}?",