Skip to content

Commit

Permalink
feat: add support for handlebars templating engine as an option
Browse files Browse the repository at this point in the history
  • Loading branch information
erise133 committed Dec 16, 2024
1 parent 67f85fb commit fe71a15
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"fastestsmallesttextencoderdecoder": "1.0.22",
"gaxios": "6.7.1",
"glob": "11.0.0",
"handlebars": "^4.7.8",
"js-sha1": "0.7.0",
"js-tiktoken": "1.0.15",
"langchain": "0.3.6",
Expand Down
48 changes: 46 additions & 2 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import handlebars from "handlebars";
import { type State } from "./types.ts";

/**
Expand All @@ -7,27 +8,70 @@ import { type State } from "./types.ts";
* It replaces each placeholder with the value from the state object that matches the placeholder's name.
* If a matching key is not found in the state object for a given placeholder, the placeholder is replaced with an empty string.
*
* 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.
*
* @param {Object} params - The parameters for composing the context.
* @param {State} params.state - The state object containing values to replace the placeholders in the template.
* @param {string} params.template - The template string containing placeholders to be replaced with state values.
* @param {"handlebars" | undefined} [params.templatingEngine] - The templating engine to use for compiling and evaluating the template (optional, default: `undefined`).
* @returns {string} The composed context string with placeholders replaced by corresponding state values.
*
* @example
* // Given a state object and a template
* const state = { userName: "Alice", userAge: 30 };
* const template = "Hello, {{userName}}! You are {{userAge}} years old";
*
* // Composing the context will result in:
* // Composing the context with simple string replacement will result in:
* // "Hello, Alice! You are 30 years old."
* const context = composeContext({ state, template });
* const contextSimple = composeContext({ state, template });
*
* // Composing the context with Handlebars templating engine will also produce the same output.
* const contextHandlebars = composeContext({ state, template, templatingEngine: 'handlebars' });
*
* // Handlebars provides more advanced features than simple string replacement.
* // For example, you can use conditionals or loops right in your template:
* const advancedTemplate = `
* {{#if userAge}}
* Hello, {{userName}}! {{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}}
* {{else}}
* Hello! We don't know your age.
* {{/if}}
*
* {{#each favoriteColors}}
* - Your favorite color: {{this}}
* {{/each}}
* `;
*
* const advancedState = {
* userName: "Alice",
* userAge: 30,
* favoriteColors: ["blue", "green", "red"]
* };
*
* // Using Handlebars with the more advanced template:
* const advancedContextHandlebars = composeContext({ state: advancedState, template: advancedTemplate, templatingEngine: 'handlebars' });
*
* // The above will produce:
* // "Hello, Alice! You are an adult.
* //
* // - Your favorite color: blue
* // - Your favorite color: green
* // - Your favorite color: red"
*/
export const composeContext = ({
state,
template,
templatingEngine,
}: {
state: State;
template: string;
templatingEngine?: "handlebars";
}) => {
if (templatingEngine === "handlebars") {
const templateFunction = handlebars.compile(template);
return templateFunction(state);
}

// @ts-expect-error match isn't working as expected
const out = template.replace(/{{\w+}}/g, (match) => {
const key = match.replace(/{{|}}/g, "");
Expand Down
198 changes: 198 additions & 0 deletions packages/core/src/tests/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { describe, expect, it } from "vitest";
import { composeContext } from "../context";
import handlebars from "handlebars";
import { State } from "../types.ts";

describe("composeContext", () => {
const baseState: State = {
actors: "",
recentMessages: "",
recentMessagesData: [],
roomId: "-----",
bio: "",
lore: "",
messageDirections: "",
postDirections: "",
userName: "",
};

// Test simple string replacement
describe("simple string replacement (default)", () => {
it("should replace placeholders with corresponding state values", () => {
const state: State = {
...baseState,
userName: "Alice",
userAge: 30,
};
const template =
"Hello, {{userName}}! You are {{userAge}} years old.";

const result = composeContext({ state, template });

expect(result).toBe("Hello, Alice! You are 30 years old.");
});

it("should replace missing state values with empty string", () => {
const state: State = {
...baseState,
userName: "Alice",
};
const template =
"Hello, {{userName}}! You are {{userAge}} years old.";

const result = composeContext({ state, template });

expect(result).toBe("Hello, Alice! You are years old.");
});

it("should handle templates with no placeholders", () => {
const state: State = {
...baseState,
userName: "Alice",
};
const template = "Hello, world!";

const result = composeContext({ state, template });

expect(result).toBe("Hello, world!");
});

it("should handle empty template", () => {
const state: State = {
...baseState,
userName: "Alice",
};
const template = "";

const result = composeContext({ state, template });

expect(result).toBe("");
});
});

// Test Handlebars templating
describe("handlebars templating", () => {
it("should process basic handlebars template", () => {
const state: State = {
...baseState,
userName: "Alice",
userAge: 30,
};
const template =
"Hello, {{userName}}! You are {{userAge}} years old.";

const result = composeContext({
state,
template,
templatingEngine: "handlebars",
});

expect(result).toBe("Hello, Alice! You are 30 years old.");
});

it("should handle handlebars conditionals", () => {
const state: State = {
...baseState,
userName: "Alice",
userAge: 30,
};
const template =
"{{#if userAge}}Age: {{userAge}}{{else}}Age unknown{{/if}}";

const result = composeContext({
state,
template,
templatingEngine: "handlebars",
});

expect(result).toBe("Age: 30");
});

it("should handle handlebars loops", () => {
const state: State = {
...baseState,
colors: ["red", "blue", "green"],
};
const template =
"{{#each colors}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}";

const result = composeContext({
state,
template,
templatingEngine: "handlebars",
});

expect(result).toBe("red, blue, green");
});

it("should handle complex handlebars template", () => {
// Register the 'gt' helper before running tests
handlebars.registerHelper("gt", function (a, b) {
return a > b;
});

const state = {
...baseState,
userName: "Alice",
userAge: 30,
favoriteColors: ["blue", "green", "red"],
};
const template = `
{{#if userAge}}
Hello, {{userName}}! {{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}}
{{else}}
Hello! We don't know your age.
{{/if}}
{{#each favoriteColors}}
- {{this}}
{{/each}}`;

const result = composeContext({
state,
template,
templatingEngine: "handlebars",
});

expect(result.trim()).toMatch(/Hello, Alice! You are an adult./);
expect(result).toContain("- blue");
expect(result).toContain("- green");
expect(result).toContain("- red");
});

it("should handle missing values in handlebars template", () => {
const state = {...baseState}
const template = "Hello, {{userName}}!";

const result = composeContext({
state,
template,
templatingEngine: "handlebars",
});

expect(result).toBe("Hello, !");
});
});

describe("error handling", () => {
it("should handle undefined state", () => {
const template = "Hello, {{userName}}!";

expect(() => {
// @ts-expect-error testing undefined state
composeContext({ template });
}).toThrow();
});

it("should handle undefined template", () => {
const state = {
...baseState,
userName: "Alice",
};

expect(() => {
// @ts-expect-error testing undefined template
composeContext({ state });
}).toThrow();
});
});
});
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fe71a15

Please sign in to comment.