Skip to content

Commit

Permalink
feat: Make templates in composeContext dynamic
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathangus committed Dec 26, 2024
1 parent 5b3385c commit b13a6f8
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 45 deletions.
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

0 comments on commit b13a6f8

Please sign in to comment.