Skip to content

Commit fe71a15

Browse files
author
erise133
committed
feat: add support for handlebars templating engine as an option
1 parent 67f85fb commit fe71a15

File tree

4 files changed

+248
-2
lines changed

4 files changed

+248
-2
lines changed

packages/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"fastestsmallesttextencoderdecoder": "1.0.22",
6565
"gaxios": "6.7.1",
6666
"glob": "11.0.0",
67+
"handlebars": "^4.7.8",
6768
"js-sha1": "0.7.0",
6869
"js-tiktoken": "1.0.15",
6970
"langchain": "0.3.6",

packages/core/src/context.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import handlebars from "handlebars";
12
import { type State } from "./types.ts";
23

34
/**
@@ -7,27 +8,70 @@ import { type State } from "./types.ts";
78
* It replaces each placeholder with the value from the state object that matches the placeholder's name.
89
* If a matching key is not found in the state object for a given placeholder, the placeholder is replaced with an empty string.
910
*
11+
* By default, this function uses a simple string replacement approach. However, when `templatingEngine` is set to `'handlebars'`, it uses Handlebars templating engine instead, compiling the template into a reusable function and evaluating it with the provided state object.
12+
*
1013
* @param {Object} params - The parameters for composing the context.
1114
* @param {State} params.state - The state object containing values to replace the placeholders in the template.
1215
* @param {string} params.template - The template string containing placeholders to be replaced with state values.
16+
* @param {"handlebars" | undefined} [params.templatingEngine] - The templating engine to use for compiling and evaluating the template (optional, default: `undefined`).
1317
* @returns {string} The composed context string with placeholders replaced by corresponding state values.
1418
*
1519
* @example
1620
* // Given a state object and a template
1721
* const state = { userName: "Alice", userAge: 30 };
1822
* const template = "Hello, {{userName}}! You are {{userAge}} years old";
1923
*
20-
* // Composing the context will result in:
24+
* // Composing the context with simple string replacement will result in:
2125
* // "Hello, Alice! You are 30 years old."
22-
* const context = composeContext({ state, template });
26+
* const contextSimple = composeContext({ state, template });
27+
*
28+
* // Composing the context with Handlebars templating engine will also produce the same output.
29+
* const contextHandlebars = composeContext({ state, template, templatingEngine: 'handlebars' });
30+
*
31+
* // Handlebars provides more advanced features than simple string replacement.
32+
* // For example, you can use conditionals or loops right in your template:
33+
* const advancedTemplate = `
34+
* {{#if userAge}}
35+
* Hello, {{userName}}! {{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}}
36+
* {{else}}
37+
* Hello! We don't know your age.
38+
* {{/if}}
39+
*
40+
* {{#each favoriteColors}}
41+
* - Your favorite color: {{this}}
42+
* {{/each}}
43+
* `;
44+
*
45+
* const advancedState = {
46+
* userName: "Alice",
47+
* userAge: 30,
48+
* favoriteColors: ["blue", "green", "red"]
49+
* };
50+
*
51+
* // Using Handlebars with the more advanced template:
52+
* const advancedContextHandlebars = composeContext({ state: advancedState, template: advancedTemplate, templatingEngine: 'handlebars' });
53+
*
54+
* // The above will produce:
55+
* // "Hello, Alice! You are an adult.
56+
* //
57+
* // - Your favorite color: blue
58+
* // - Your favorite color: green
59+
* // - Your favorite color: red"
2360
*/
2461
export const composeContext = ({
2562
state,
2663
template,
64+
templatingEngine,
2765
}: {
2866
state: State;
2967
template: string;
68+
templatingEngine?: "handlebars";
3069
}) => {
70+
if (templatingEngine === "handlebars") {
71+
const templateFunction = handlebars.compile(template);
72+
return templateFunction(state);
73+
}
74+
3175
// @ts-expect-error match isn't working as expected
3276
const out = template.replace(/{{\w+}}/g, (match) => {
3377
const key = match.replace(/{{|}}/g, "");
+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { describe, expect, it } from "vitest";
2+
import { composeContext } from "../context";
3+
import handlebars from "handlebars";
4+
import { State } from "../types.ts";
5+
6+
describe("composeContext", () => {
7+
const baseState: State = {
8+
actors: "",
9+
recentMessages: "",
10+
recentMessagesData: [],
11+
roomId: "-----",
12+
bio: "",
13+
lore: "",
14+
messageDirections: "",
15+
postDirections: "",
16+
userName: "",
17+
};
18+
19+
// Test simple string replacement
20+
describe("simple string replacement (default)", () => {
21+
it("should replace placeholders with corresponding state values", () => {
22+
const state: State = {
23+
...baseState,
24+
userName: "Alice",
25+
userAge: 30,
26+
};
27+
const template =
28+
"Hello, {{userName}}! You are {{userAge}} years old.";
29+
30+
const result = composeContext({ state, template });
31+
32+
expect(result).toBe("Hello, Alice! You are 30 years old.");
33+
});
34+
35+
it("should replace missing state values with empty string", () => {
36+
const state: State = {
37+
...baseState,
38+
userName: "Alice",
39+
};
40+
const template =
41+
"Hello, {{userName}}! You are {{userAge}} years old.";
42+
43+
const result = composeContext({ state, template });
44+
45+
expect(result).toBe("Hello, Alice! You are years old.");
46+
});
47+
48+
it("should handle templates with no placeholders", () => {
49+
const state: State = {
50+
...baseState,
51+
userName: "Alice",
52+
};
53+
const template = "Hello, world!";
54+
55+
const result = composeContext({ state, template });
56+
57+
expect(result).toBe("Hello, world!");
58+
});
59+
60+
it("should handle empty template", () => {
61+
const state: State = {
62+
...baseState,
63+
userName: "Alice",
64+
};
65+
const template = "";
66+
67+
const result = composeContext({ state, template });
68+
69+
expect(result).toBe("");
70+
});
71+
});
72+
73+
// Test Handlebars templating
74+
describe("handlebars templating", () => {
75+
it("should process basic handlebars template", () => {
76+
const state: State = {
77+
...baseState,
78+
userName: "Alice",
79+
userAge: 30,
80+
};
81+
const template =
82+
"Hello, {{userName}}! You are {{userAge}} years old.";
83+
84+
const result = composeContext({
85+
state,
86+
template,
87+
templatingEngine: "handlebars",
88+
});
89+
90+
expect(result).toBe("Hello, Alice! You are 30 years old.");
91+
});
92+
93+
it("should handle handlebars conditionals", () => {
94+
const state: State = {
95+
...baseState,
96+
userName: "Alice",
97+
userAge: 30,
98+
};
99+
const template =
100+
"{{#if userAge}}Age: {{userAge}}{{else}}Age unknown{{/if}}";
101+
102+
const result = composeContext({
103+
state,
104+
template,
105+
templatingEngine: "handlebars",
106+
});
107+
108+
expect(result).toBe("Age: 30");
109+
});
110+
111+
it("should handle handlebars loops", () => {
112+
const state: State = {
113+
...baseState,
114+
colors: ["red", "blue", "green"],
115+
};
116+
const template =
117+
"{{#each colors}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}";
118+
119+
const result = composeContext({
120+
state,
121+
template,
122+
templatingEngine: "handlebars",
123+
});
124+
125+
expect(result).toBe("red, blue, green");
126+
});
127+
128+
it("should handle complex handlebars template", () => {
129+
// Register the 'gt' helper before running tests
130+
handlebars.registerHelper("gt", function (a, b) {
131+
return a > b;
132+
});
133+
134+
const state = {
135+
...baseState,
136+
userName: "Alice",
137+
userAge: 30,
138+
favoriteColors: ["blue", "green", "red"],
139+
};
140+
const template = `
141+
{{#if userAge}}
142+
Hello, {{userName}}! {{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}}
143+
{{else}}
144+
Hello! We don't know your age.
145+
{{/if}}
146+
{{#each favoriteColors}}
147+
- {{this}}
148+
{{/each}}`;
149+
150+
const result = composeContext({
151+
state,
152+
template,
153+
templatingEngine: "handlebars",
154+
});
155+
156+
expect(result.trim()).toMatch(/Hello, Alice! You are an adult./);
157+
expect(result).toContain("- blue");
158+
expect(result).toContain("- green");
159+
expect(result).toContain("- red");
160+
});
161+
162+
it("should handle missing values in handlebars template", () => {
163+
const state = {...baseState}
164+
const template = "Hello, {{userName}}!";
165+
166+
const result = composeContext({
167+
state,
168+
template,
169+
templatingEngine: "handlebars",
170+
});
171+
172+
expect(result).toBe("Hello, !");
173+
});
174+
});
175+
176+
describe("error handling", () => {
177+
it("should handle undefined state", () => {
178+
const template = "Hello, {{userName}}!";
179+
180+
expect(() => {
181+
// @ts-expect-error testing undefined state
182+
composeContext({ template });
183+
}).toThrow();
184+
});
185+
186+
it("should handle undefined template", () => {
187+
const state = {
188+
...baseState,
189+
userName: "Alice",
190+
};
191+
192+
expect(() => {
193+
// @ts-expect-error testing undefined template
194+
composeContext({ state });
195+
}).toThrow();
196+
});
197+
});
198+
});

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)