template.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import mustache from "mustache";
  2. import { addLangChainErrorFields } from "../errors/index.js";
  3. function configureMustache() {
  4. // Use unescaped HTML
  5. // https://github.com/janl/mustache.js?tab=readme-ov-file#variables
  6. mustache.escape = (text) => text;
  7. }
  8. export const parseFString = (template) => {
  9. // Core logic replicated from internals of pythons built in Formatter class.
  10. // https://github.com/python/cpython/blob/135ec7cefbaffd516b77362ad2b2ad1025af462e/Objects/stringlib/unicode_format.h#L700-L706
  11. const chars = template.split("");
  12. const nodes = [];
  13. const nextBracket = (bracket, start) => {
  14. for (let i = start; i < chars.length; i += 1) {
  15. if (bracket.includes(chars[i])) {
  16. return i;
  17. }
  18. }
  19. return -1;
  20. };
  21. let i = 0;
  22. while (i < chars.length) {
  23. if (chars[i] === "{" && i + 1 < chars.length && chars[i + 1] === "{") {
  24. nodes.push({ type: "literal", text: "{" });
  25. i += 2;
  26. }
  27. else if (chars[i] === "}" &&
  28. i + 1 < chars.length &&
  29. chars[i + 1] === "}") {
  30. nodes.push({ type: "literal", text: "}" });
  31. i += 2;
  32. }
  33. else if (chars[i] === "{") {
  34. const j = nextBracket("}", i);
  35. if (j < 0) {
  36. throw new Error("Unclosed '{' in template.");
  37. }
  38. nodes.push({
  39. type: "variable",
  40. name: chars.slice(i + 1, j).join(""),
  41. });
  42. i = j + 1;
  43. }
  44. else if (chars[i] === "}") {
  45. throw new Error("Single '}' in template.");
  46. }
  47. else {
  48. const next = nextBracket("{}", i);
  49. const text = (next < 0 ? chars.slice(i) : chars.slice(i, next)).join("");
  50. nodes.push({ type: "literal", text });
  51. i = next < 0 ? chars.length : next;
  52. }
  53. }
  54. return nodes;
  55. };
  56. /**
  57. * Convert the result of mustache.parse into an array of ParsedTemplateNode,
  58. * to make it compatible with other LangChain string parsing template formats.
  59. *
  60. * @param {mustache.TemplateSpans} template The result of parsing a mustache template with the mustache.js library.
  61. * @returns {ParsedTemplateNode[]}
  62. */
  63. const mustacheTemplateToNodes = (template) => template.map((temp) => {
  64. if (temp[0] === "name") {
  65. const name = temp[1].includes(".") ? temp[1].split(".")[0] : temp[1];
  66. return { type: "variable", name };
  67. }
  68. else if (["#", "&", "^", ">"].includes(temp[0])) {
  69. // # represents a section, "&" represents an unescaped variable.
  70. // These should both be considered variables.
  71. return { type: "variable", name: temp[1] };
  72. }
  73. else {
  74. return { type: "literal", text: temp[1] };
  75. }
  76. });
  77. export const parseMustache = (template) => {
  78. configureMustache();
  79. const parsed = mustache.parse(template);
  80. return mustacheTemplateToNodes(parsed);
  81. };
  82. export const interpolateFString = (template, values) => {
  83. return parseFString(template).reduce((res, node) => {
  84. if (node.type === "variable") {
  85. if (node.name in values) {
  86. const stringValue = typeof values[node.name] === "string"
  87. ? values[node.name]
  88. : JSON.stringify(values[node.name]);
  89. return res + stringValue;
  90. }
  91. throw new Error(`(f-string) Missing value for input ${node.name}`);
  92. }
  93. return res + node.text;
  94. }, "");
  95. };
  96. export const interpolateMustache = (template, values) => {
  97. configureMustache();
  98. return mustache.render(template, values);
  99. };
  100. export const DEFAULT_FORMATTER_MAPPING = {
  101. "f-string": interpolateFString,
  102. mustache: interpolateMustache,
  103. };
  104. export const DEFAULT_PARSER_MAPPING = {
  105. "f-string": parseFString,
  106. mustache: parseMustache,
  107. };
  108. export const renderTemplate = (template, templateFormat, inputValues) => {
  109. try {
  110. return DEFAULT_FORMATTER_MAPPING[templateFormat](template, inputValues);
  111. }
  112. catch (e) {
  113. const error = addLangChainErrorFields(e, "INVALID_PROMPT_INPUT");
  114. throw error;
  115. }
  116. };
  117. export const parseTemplate = (template, templateFormat) => DEFAULT_PARSER_MAPPING[templateFormat](template);
  118. export const checkValidTemplate = (template, templateFormat, inputVariables) => {
  119. if (!(templateFormat in DEFAULT_FORMATTER_MAPPING)) {
  120. const validFormats = Object.keys(DEFAULT_FORMATTER_MAPPING);
  121. throw new Error(`Invalid template format. Got \`${templateFormat}\`;
  122. should be one of ${validFormats}`);
  123. }
  124. try {
  125. const dummyInputs = inputVariables.reduce((acc, v) => {
  126. acc[v] = "foo";
  127. return acc;
  128. }, {});
  129. if (Array.isArray(template)) {
  130. template.forEach((message) => {
  131. if (message.type === "text") {
  132. renderTemplate(message.text, templateFormat, dummyInputs);
  133. }
  134. else if (message.type === "image_url") {
  135. if (typeof message.image_url === "string") {
  136. renderTemplate(message.image_url, templateFormat, dummyInputs);
  137. }
  138. else {
  139. const imageUrl = message.image_url.url;
  140. renderTemplate(imageUrl, templateFormat, dummyInputs);
  141. }
  142. }
  143. else {
  144. throw new Error(`Invalid message template received. ${JSON.stringify(message, null, 2)}`);
  145. }
  146. });
  147. }
  148. else {
  149. renderTemplate(template, templateFormat, dummyInputs);
  150. }
  151. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  152. }
  153. catch (e) {
  154. throw new Error(`Invalid prompt schema: ${e.message}`);
  155. }
  156. };