Skip to content

Commit 9348c97

Browse files
authored
Merge pull request elizaOS#1136 from erise133/feat/handlebars-templating-engine
feat: add support for handlebars templating engine as an option
2 parents d0ea220 + 4b98e2b commit 9348c97

File tree

5 files changed

+279
-25
lines changed

5 files changed

+279
-25
lines changed

docs/docs/api/functions/composeContext.md

+64-23
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,85 @@
22

33
> **composeContext**(`params`): `string`
44
5-
Composes a context string by replacing placeholders in a template with corresponding values from the state.
6-
7-
This function takes a template string with placeholders in the format `{{placeholder}}` and a state object.
8-
It replaces each placeholder with the value from the state object that matches the placeholder's name.
9-
If a matching key is not found in the state object for a given placeholder, the placeholder is replaced with an empty string.
5+
Composes a context string by replacing placeholders in a template with values from a state object. Supports both simple string replacement and the Handlebars templating engine.
106

117
## Parameters
128

13-
**params**
14-
15-
The parameters for composing the context.
9+
### **params**: `Object`
1610

17-
**params.state**: [`State`](../interfaces/State.md)
11+
An object containing the following properties:
1812

19-
The state object containing values to replace the placeholders in the template.
13+
- **state**: `State`
14+
The state object containing key-value pairs for replacing placeholders in the template.
2015

21-
**params.template**: `string`
16+
- **template**: `string`
17+
A string containing placeholders in the format `{{placeholder}}`.
2218

23-
The template string containing placeholders to be replaced with state values.
19+
- **templatingEngine**: `"handlebars" | undefined` *(optional)*
20+
The templating engine to use. If set to `"handlebars"`, the Handlebars engine is used for template compilation. Defaults to `undefined` (simple string replacement).
2421

2522
## Returns
2623

2724
`string`
2825

29-
The composed context string with placeholders replaced by corresponding state values.
26+
The context string with placeholders replaced by corresponding values from the state object. If a placeholder has no matching key in the state, it is replaced with an empty string.
27+
28+
## Examples
3029

31-
## Example
30+
### Simple Example
3231

33-
```ts
34-
// Given a state object and a template
32+
```javascript
3533
const state = { userName: "Alice", userAge: 30 };
36-
const template = "Hello, {{userName}}! You are {{userAge}} years old";
34+
const template = "Hello, {{userName}}! You are {{userAge}} years old.";
3735

38-
// Composing the context will result in:
39-
// "Hello, Alice! You are 30 years old."
40-
const context = composeContext({ state, template });
41-
```
36+
// Simple string replacement
37+
const contextSimple = composeContext({ state, template });
38+
// Output: "Hello, Alice! You are 30 years old."
4239

43-
## Defined in
40+
// Handlebars templating
41+
const contextHandlebars = composeContext({ state, template, templatingEngine: 'handlebars' });
42+
// Output: "Hello, Alice! You are 30 years old."
43+
```
4444

45-
[packages/core/src/context.ts:24](https://github.com/ai16z/eliza/blob/7fcf54e7fb2ba027d110afcc319c0b01b3f181dc/packages/core/src/context.ts#L24)
45+
### Advanced Example
46+
47+
```javascript
48+
const advancedTemplate = `
49+
{{#if userAge}}
50+
Hello, {{userName}}!
51+
{{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}}
52+
{{else}}
53+
Hello! We don't know your age.
54+
{{/if}}
55+
56+
{{#if favoriteColors.length}}
57+
Your favorite colors are:
58+
{{#each favoriteColors}}
59+
- {{this}}
60+
{{/each}}
61+
{{else}}
62+
You didn't specify any favorite colors.
63+
{{/if}}
64+
`;
65+
66+
const advancedState = {
67+
userName: "Alice",
68+
userAge: 30,
69+
favoriteColors: ["blue", "green", "red"]
70+
};
71+
72+
// Composing the context with Handlebars
73+
const advancedContextHandlebars = composeContext({
74+
state: advancedState,
75+
template: advancedTemplate,
76+
templatingEngine: 'handlebars'
77+
});
78+
// Output:
79+
// Hello, Alice!
80+
// You are an adult.
81+
//
82+
// Your favorite colors are:
83+
// - blue
84+
// - green
85+
// - red
86+
```

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

+13-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,37 @@ 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 });
2327
*/
2428
export const composeContext = ({
2529
state,
2630
template,
31+
templatingEngine,
2732
}: {
2833
state: State;
2934
template: string;
35+
templatingEngine?: "handlebars";
3036
}) => {
37+
if (templatingEngine === "handlebars") {
38+
const templateFunction = handlebars.compile(template);
39+
return templateFunction(state);
40+
}
41+
3142
// @ts-expect-error match isn't working as expected
3243
const out = template.replace(/{{\w+}}/g, (match) => {
3344
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)