Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make templates in composeContext dynamic #1467

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/api/functions/composeContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Function: composeContext()

> **composeContext**(`params`): `any`
> **composeContext**(`params`): `string`

Composes a context string by replacing placeholders in a template with corresponding values from the state.

Expand All @@ -22,17 +22,17 @@ 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"`

The templating engine to use for compiling and evaluating the template (optional, default: `undefined`).

## Returns

`any`
`string`

The composed context string with placeholders replaced by corresponding state values.

Expand Down
20 changes: 12 additions & 8 deletions docs/docs/api/functions/composeContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."
```

Expand All @@ -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.
Expand All @@ -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!
Expand Down
7 changes: 4 additions & 3 deletions packages/client-twitter/src/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ModelClass,
stringToUuid,
parseBooleanFromText,
TemplateType,
} from "@elizaos/core";
import { elizaLogger } from "@elizaos/core";
import { ClientBase } from "./base.ts";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -379,7 +380,7 @@ export class TwitterPostClient {
private async generateTweetContent(
tweetState: any,
options?: {
template?: string;
template?: TemplateType;
context?: string;
}
): Promise<string> {
Expand Down
21 changes: 16 additions & 5 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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.
*
Expand All @@ -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] ?? "";
});
Expand Down
92 changes: 91 additions & 1 deletion packages/core/src/tests/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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({
Expand Down
50 changes: 26 additions & 24 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,8 @@ export interface ModelConfiguration {
maxInputTokens?: number;
}

export type TemplateType = string | ((options: { state: State }) => string);

/**
* Configuration for an agent character
*/
Expand Down Expand Up @@ -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 */
Expand Down
Loading