From 372fe8231198973e573f12310ad77533f3b3eda0 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Thu, 26 Dec 2024 13:47:25 +0100 Subject: [PATCH] feat: Make templates in composeContext dynamic --- docs/api/functions/composeContext.md | 4 +- docs/docs/api/functions/composeContext.md | 20 +++-- packages/client-twitter/src/post.ts | 7 +- packages/core/src/context.ts | 21 ++++-- packages/core/src/tests/context.test.ts | 92 ++++++++++++++++++++++- packages/core/src/types.ts | 50 ++++++------ 6 files changed, 151 insertions(+), 43 deletions(-) diff --git a/docs/api/functions/composeContext.md b/docs/api/functions/composeContext.md index 4d3ae6ea39..767f8fba4a 100644 --- a/docs/api/functions/composeContext.md +++ b/docs/api/functions/composeContext.md @@ -22,9 +22,9 @@ The parameters for composing the context. The state object containing values to replace the placeholders in the template. -• **params.template**: `string` +• **params.template**: `string` | `Function` -The template string containing placeholders to be replaced with state values. +The template string or function returning a string containing placeholders to be replaced with state values. • **params.templatingEngine?**: `"handlebars"` diff --git a/docs/docs/api/functions/composeContext.md b/docs/docs/api/functions/composeContext.md index 7ff2652bc2..cde69d271c 100644 --- a/docs/docs/api/functions/composeContext.md +++ b/docs/docs/api/functions/composeContext.md @@ -10,13 +10,13 @@ Composes a context string by replacing placeholders in a template with values fr An object containing the following properties: -- **state**: `State` +- **state**: `State` The state object containing key-value pairs for replacing placeholders in the template. -- **template**: `string` - A string containing placeholders in the format `{{placeholder}}`. +- **template**: `string | Function` + A string or function returning a string containing placeholders in the format `{{placeholder}}`. -- **templatingEngine**: `"handlebars" | undefined` *(optional)* +- **templatingEngine**: `"handlebars" | undefined` _(optional)_ The templating engine to use. If set to `"handlebars"`, the Handlebars engine is used for template compilation. Defaults to `undefined` (simple string replacement). ## Returns @@ -38,7 +38,11 @@ const contextSimple = composeContext({ state, template }); // Output: "Hello, Alice! You are 30 years old." // Handlebars templating -const contextHandlebars = composeContext({ state, template, templatingEngine: 'handlebars' }); +const contextHandlebars = composeContext({ + state, + template, + templatingEngine: "handlebars", +}); // Output: "Hello, Alice! You are 30 years old." ``` @@ -47,7 +51,7 @@ const contextHandlebars = composeContext({ state, template, templatingEngine: 'h ```javascript const advancedTemplate = ` {{#if userAge}} - Hello, {{userName}}! + Hello, {{userName}}! {{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}} {{else}} Hello! We don't know your age. @@ -66,14 +70,14 @@ const advancedTemplate = ` const advancedState = { userName: "Alice", userAge: 30, - favoriteColors: ["blue", "green", "red"] + favoriteColors: ["blue", "green", "red"], }; // Composing the context with Handlebars const advancedContextHandlebars = composeContext({ state: advancedState, template: advancedTemplate, - templatingEngine: 'handlebars' + templatingEngine: "handlebars", }); // Output: // Hello, Alice! diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 03c332a18f..08831578fb 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -7,6 +7,7 @@ import { ModelClass, stringToUuid, parseBooleanFromText, + TemplateType, } from "@elizaos/core"; import { elizaLogger } from "@elizaos/core"; import { ClientBase } from "./base.ts"; @@ -164,8 +165,8 @@ export class TwitterPostClient { this.runtime.getSetting("POST_IMMEDIATELY") !== "" ) { // Retrieve setting, default to false if not set or if the value is not "true" - postImmediately = this.runtime.getSetting("POST_IMMEDIATELY") === "true" || false; - + postImmediately = + this.runtime.getSetting("POST_IMMEDIATELY") === "true" || false; } if (postImmediately) { @@ -379,7 +380,7 @@ export class TwitterPostClient { private async generateTweetContent( tweetState: any, options?: { - template?: string; + template?: TemplateType; context?: string; } ): Promise { diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index a682e6794c..059e302d62 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -1,5 +1,5 @@ import handlebars from "handlebars"; -import { type State } from "./types.ts"; +import { type State, type TemplateType } from "./types.ts"; import { names, uniqueNamesGenerator } from "unique-names-generator"; /** @@ -13,7 +13,7 @@ import { names, uniqueNamesGenerator } from "unique-names-generator"; * * @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 {TemplateType} params.template - The template string or function 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. * @@ -25,23 +25,34 @@ import { names, uniqueNamesGenerator } from "unique-names-generator"; * // Composing the context with simple string replacement will result in: * // "Hello, Alice! You are 30 years old." * const contextSimple = composeContext({ state, template }); + * + * // Using composeContext with a template function for dynamic template + * const template = ({ state }) => { + * const tone = Math.random() > 0.5 ? "kind" : "rude"; + * return `Hello, {{userName}}! You are {{userAge}} years old. Be ${tone}`; + * }; + * const contextSimple = composeContext({ state, template }); */ + export const composeContext = ({ state, template, templatingEngine, }: { state: State; - template: string; + template: TemplateType; templatingEngine?: "handlebars"; }) => { + const templateStr = + typeof template === "function" ? template({ state }) : template; + if (templatingEngine === "handlebars") { - const templateFunction = handlebars.compile(template); + const templateFunction = handlebars.compile(templateStr); return templateFunction(state); } // @ts-expect-error match isn't working as expected - const out = template.replace(/{{\w+}}/g, (match) => { + const out = templateStr.replace(/{{\w+}}/g, (match) => { const key = match.replace(/{{|}}/g, ""); return state[key] ?? ""; }); diff --git a/packages/core/src/tests/context.test.ts b/packages/core/src/tests/context.test.ts index 6bf391282b..3c3bc978f9 100644 --- a/packages/core/src/tests/context.test.ts +++ b/packages/core/src/tests/context.test.ts @@ -70,6 +70,96 @@ describe("composeContext", () => { }); }); + describe("dynamic templates", () => { + it("should handle function templates", () => { + const state: State = { + ...baseState, + userName: "Alice", + userAge: 30, + }; + const template = () => { + return "Hello, {{userName}}! You are {{userAge}} years old."; + }; + + const result = composeContext({ state, template }); + + expect(result).toBe("Hello, Alice! You are 30 years old."); + }); + + it("should handle function templates with conditional logic", () => { + const state: State = { + ...baseState, + userName: "Alice", + userAge: 30, + }; + const isEdgy = true; + const template = () => { + if (isEdgy) { + return "Hello, {{userName}}! You are {{userAge}} years old... whatever"; + } + + return `Hello, {{userName}}! You are {{userAge}} years old`; + }; + + const result = composeContext({ state, template }); + + expect(result).toBe( + "Hello, Alice! You are 30 years old... whatever" + ); + }); + + it("should handle function templates with conditional logic depending on state", () => { + const template = ({ state }: { state: State }) => { + if (state.userName) { + return `Hello, {{userName}}! You are {{userAge}} years old.`; + } + + return `Hello, anon! You are {{userAge}} years old.`; + }; + + const result = composeContext({ + state: { + ...baseState, + userName: "Alice", + userAge: 30, + }, + template, + }); + + const resultWithoutUsername = composeContext({ + state: { + ...baseState, + userAge: 30, + }, + template, + }); + + expect(result).toBe("Hello, Alice! You are 30 years old."); + expect(resultWithoutUsername).toBe( + "Hello, anon! You are 30 years old." + ); + }); + + it("should handle function templates with handlebars templating engine", () => { + const state: State = { + ...baseState, + userName: "Alice", + userAge: 30, + }; + const template = () => { + return `{{#if userAge}}Hello, {{userName}}!{{else}}Hi there!{{/if}}`; + }; + + const result = composeContext({ + state, + template, + templatingEngine: "handlebars", + }); + + expect(result).toBe("Hello, Alice!"); + }); + }); + // Test Handlebars templating describe("handlebars templating", () => { it("should process basic handlebars template", () => { @@ -160,7 +250,7 @@ describe("composeContext", () => { }); it("should handle missing values in handlebars template", () => { - const state = {...baseState} + const state = { ...baseState }; const template = "Hello, {{userName}}!"; const result = composeContext({ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6786bb99f9..a8aa3523e0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -634,6 +634,8 @@ export interface ModelConfiguration { maxInputTokens?: number; } +export type TemplateType = string | ((options: { state: State }) => string); + /** * Configuration for an agent character */ @@ -661,30 +663,30 @@ export type Character = { /** Optional prompt templates */ templates?: { - goalsTemplate?: string; - factsTemplate?: string; - messageHandlerTemplate?: string; - shouldRespondTemplate?: string; - continueMessageHandlerTemplate?: string; - evaluationTemplate?: string; - twitterSearchTemplate?: string; - twitterActionTemplate?: string; - twitterPostTemplate?: string; - twitterMessageHandlerTemplate?: string; - twitterShouldRespondTemplate?: string; - farcasterPostTemplate?: string; - lensPostTemplate?: string; - farcasterMessageHandlerTemplate?: string; - lensMessageHandlerTemplate?: string; - farcasterShouldRespondTemplate?: string; - lensShouldRespondTemplate?: string; - telegramMessageHandlerTemplate?: string; - telegramShouldRespondTemplate?: string; - discordVoiceHandlerTemplate?: string; - discordShouldRespondTemplate?: string; - discordMessageHandlerTemplate?: string; - slackMessageHandlerTemplate?: string; - slackShouldRespondTemplate?: string; + goalsTemplate?: TemplateType; + factsTemplate?: TemplateType; + messageHandlerTemplate?: TemplateType; + shouldRespondTemplate?: TemplateType; + continueMessageHandlerTemplate?: TemplateType; + evaluationTemplate?: TemplateType; + twitterSearchTemplate?: TemplateType; + twitterActionTemplate?: TemplateType; + twitterPostTemplate?: TemplateType; + twitterMessageHandlerTemplate?: TemplateType; + twitterShouldRespondTemplate?: TemplateType; + farcasterPostTemplate?: TemplateType; + lensPostTemplate?: TemplateType; + farcasterMessageHandlerTemplate?: TemplateType; + lensMessageHandlerTemplate?: TemplateType; + farcasterShouldRespondTemplate?: TemplateType; + lensShouldRespondTemplate?: TemplateType; + telegramMessageHandlerTemplate?: TemplateType; + telegramShouldRespondTemplate?: TemplateType; + discordVoiceHandlerTemplate?: TemplateType; + discordShouldRespondTemplate?: TemplateType; + discordMessageHandlerTemplate?: TemplateType; + slackMessageHandlerTemplate?: TemplateType; + slackShouldRespondTemplate?: TemplateType; }; /** Character biography */