Skip to content
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
12 changes: 12 additions & 0 deletions js/render-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ describe("renderMessages", () => {
const rendered = renderMessages(messages, {});
expect(rendered[0].content).toBe("");
});

it("should explain unclosed tags with line and column", () => {
const messages: ChatCompletionMessageParam[] = [
{
role: "user",
content: "Grade this.\nOutput: {{output}}\nExpected: {{expected",
},
];
expect(() => renderMessages(messages, { output: "x" })).toThrow(
/Unclosed tag at \d+ \(line 3, column \d+, near/,
);
});
});

describe("renderMessages with thread variables", () => {
Expand Down
40 changes: 33 additions & 7 deletions js/render-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,42 @@ function escapeValue(v: unknown): string {
return JSON.stringify(v);
}

function explainMustacheError(error: unknown, template: string): unknown {
if (!(error instanceof Error)) {
return error;
}
const match = error.message.match(/at (\d+)$/);
if (!match) {
return error;
}
const offset = Number(match[1]);
const before = template.slice(0, offset);
const line = before.split("\n").length;
const column = offset - before.lastIndexOf("\n");
const snippet = template.slice(Math.max(0, offset - 30), offset + 10);
return new Error(
`${error.message} (line ${line}, column ${column}, near "…${snippet}…"). ` +
"Check for an unclosed {{ }} tag or {{#section}} block in the template.",
);
}

export function renderMessages(
messages: ChatCompletionMessageParam[],
renderArgs: Record<string, unknown>,
): ChatCompletionMessageParam[] {
return messages.map((m) => ({
...m,
content: m.content
? mustache.render(m.content as string, renderArgs, undefined, {
return messages.map((m) => {
if (!m.content) {
return { ...m, content: "" };
}
try {
return {
...m,
content: mustache.render(m.content as string, renderArgs, undefined, {
escape: escapeValue,
})
: "",
}));
}),
};
} catch (e) {
throw explainMustacheError(e, String(m.content));
}
});
}
Loading