From 34ac329a895d650bc3d95f7c719f7bcbb99d5416 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:30:25 -0500 Subject: [PATCH 1/4] feat: add batch msgs & prompt templating --- packages/daemon/src/daemon.ts | 257 +++++++++++++++++++++++++- packages/daemon/src/llm.ts | 156 +++++++++++++++- packages/daemon/src/templateParser.ts | 61 ++++++ packages/daemon/src/types.ts | 4 +- 4 files changed, 467 insertions(+), 11 deletions(-) create mode 100644 packages/daemon/src/templateParser.ts diff --git a/packages/daemon/src/daemon.ts b/packages/daemon/src/daemon.ts index c1d92d4..8deb357 100644 --- a/packages/daemon/src/daemon.ts +++ b/packages/daemon/src/daemon.ts @@ -11,7 +11,13 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "./SSEClientTransport.js"; import type { TextContent } from "@modelcontextprotocol/sdk/types.js"; import type { Keypair } from "@solana/web3.js"; -import { createPrompt, generateText } from "./llm.js"; +import { + createMultiplePrompts, + createPrompt, + generateEmbeddings, + generateText, + generateTextWithMessages, +} from "./llm.js"; import nacl from "tweetnacl"; import { nanoid } from "nanoid"; import { Buffer } from "buffer"; @@ -66,7 +72,9 @@ export class Daemon implements IDaemon { ) { this.modelApiKeys = { generationKey: opts.modelApiKeys.generationKey, - } + embeddingKey: + opts.modelApiKeys.embeddingKey ?? opts.modelApiKeys.generationKey, + }; this.keypair = opts.privateKey; @@ -270,6 +278,42 @@ export class Daemon implements IDaemon { toolArgs?: { [key: string]: any; // key = `serverUrl-toolName` }; + /** + * Use a custom system prompt instead of the default one + */ + customSystemPrompt?: string; + /** + * Opt to use a custom message template instead of the default one. + * + * This involves passing a string with the following placeholders: + * - {{name}} + * - {{identityPrompt}} + * - {{message}} + * - {{context}} + * - {{tools}} + * + * If any of these placeholders are missing, then that section will be omitted. + * + * @example + * ```typescript + * const userTemplate = ` + * # Name + * {{name}} + * + * # Identity + * {{identity}} + * + * # Message + * {{message}} + * + * # Context + * {{context}} + * + * # Tools + * {{tools}} + * `; + */ + customMessageTemplate?: string; } ): Promise { if (!this.keypair) { @@ -339,11 +383,216 @@ export class Daemon implements IDaemon { } // Generate Text - lifecycle.generatedPrompt = createPrompt(lifecycle); + lifecycle.generatedPrompt = createPrompt( + lifecycle, + opts?.customMessageTemplate + ); lifecycle.output = await generateText( this.character.modelSettings.generation, this.modelApiKeys.generationKey, - lifecycle.generatedPrompt + lifecycle.generatedPrompt, + opts?.customSystemPrompt + ); + + if (actions) { + let actionPromises: Promise[] = []; + for (const tool of this.tools.action) { + const toolArgs = + opts?.toolArgs?.[`${tool.serverUrl}-${tool.tool.name}`]; + actionPromises.push( + this.callTool(tool.tool.name, tool.serverUrl, { + lifecycle, + args: toolArgs, + }) + ); + } + + const actionResults = await Promise.all(actionPromises); + lifecycle.actionsLog = actionResults + .map((lfcyl) => { + return lfcyl.actionsLog; + }) + .flat(); + lifecycle.hooks = actionResults + .map((lfcyl) => { + return lfcyl.hooks; + }) + .flat(); + + let hookPromises: Promise[] = []; + for (const hook of lifecycle.hooks) { + hookPromises.push(this.hook(hook)); + } + + const hookResults = await Promise.all(hookPromises); + lifecycle.hooksLog = hookResults + .map((hookResult) => { + return hookResult; + }) + .flat(); + } + + if (postProcess) { + let postProcessPromises: Promise[] = []; + for (const tool of this.tools.postProcess) { + const toolArgs = + opts?.toolArgs?.[`${tool.serverUrl}-${tool.tool.name}`]; + postProcessPromises.push( + this.callTool(tool.tool.name, tool.serverUrl, { + lifecycle, + args: toolArgs, + }) + ); + } + + const postProcessResults = await Promise.all(postProcessPromises); + lifecycle.postProcessLog = postProcessResults + .map((lfcyl) => { + return lfcyl.postProcessLog; + }) + .flat(); + } + + return lifecycle; + } + + async multipleMessages( + messages: { + role: "user" | "assistant"; + content: string; + }[], + opts?: { + channelId?: string; + context?: boolean; + actions?: boolean; + postProcess?: boolean; + toolArgs?: { + [key: string]: any; // key = `serverUrl-toolName` + }; + /** + * Use a custom system prompt instead of the default one + */ + customSystemPrompt?: string; + /** + * Opt to use a custom message template instead of the default one. + * + * This involves passing a string with the following placeholders: + * - {{name}} + * - {{identityPrompt}} + * - {{message}} + * - {{context}} + * - {{tools}} + * + * If any of these placeholders are missing, then that section will be omitted. + * + * @example + * ```typescript + * const userTemplate = ` + * # Name + * {{name}} + * + * # Identity + * {{identity}} + * + * # Message + * {{message}} + * + * # Context + * {{context}} + * + * # Tools + * {{tools}} + * `; + */ + customMessageTemplate?: string; + } + ): Promise { + if (!this.keypair) { + throw new Error("Keypair not found"); + } + + if (!this.character) { + throw new Error("Character not found"); + } + + if (!this.modelApiKeys.embeddingKey || !this.modelApiKeys.generationKey) { + throw new Error("Model API keys not found"); + } + + const context = opts?.context ?? true; + const actions = opts?.actions ?? true; + const postProcess = opts?.postProcess ?? true; + + const formattedMessages = messages.map( + (m) => ` + # ${m.role} + ${m.content} + ` + ); + + // Lifecycle: message -> fetchContext -> generateText -> takeActions -> hooks -> callHooks -> postProcess + let lifecycle: IMessageLifecycle = { + daemonPubkey: this.keypair.publicKey.toBase58(), + daemonName: this.character?.name ?? "", + messageId: nanoid(), + message: formattedMessages, + createdAt: new Date().toISOString(), + approval: "", + channelId: opts?.channelId ?? null, + identityPrompt: + this.character?.identityPrompt ?? DEFAULT_IDENTITY_PROMPT(this), + context: [], + tools: [], + generatedPrompt: "", + output: "", + hooks: [], + hooksLog: [], + actionsLog: [], + postProcessLog: [], + }; + + // Generate Approval + lifecycle = this.generateApproval(lifecycle); + + if (context) { + let contextPromises: Promise[] = []; + for (const tool of this.tools.context) { + const toolArgs = + opts?.toolArgs?.[`${tool.serverUrl}-${tool.tool.name}`]; + contextPromises.push( + this.callTool(tool.tool.name, tool.serverUrl, { + lifecycle, + args: toolArgs, + }) + ); + } + + const contextResults = await Promise.all(contextPromises); + lifecycle.context = contextResults + .map((lfcyl) => { + return lfcyl.context; + }) + .flat(); + lifecycle.tools = contextResults + .map((lfcyl) => { + return lfcyl.tools; + }) + .flat(); + } + + // Construct messages with custom prompt if provided + const prompts = createMultiplePrompts( + lifecycle, + messages, + opts?.customMessageTemplate + ); + lifecycle.generatedPrompt = prompts.map((p) => p.content); + // Generate Text given multiple messages + lifecycle.output = await generateTextWithMessages( + this.character.modelSettings.generation, + this.modelApiKeys.generationKey, + prompts, + opts?.customSystemPrompt ); if (actions) { diff --git a/packages/daemon/src/llm.ts b/packages/daemon/src/llm.ts index 74a9ef5..a835ce4 100644 --- a/packages/daemon/src/llm.ts +++ b/packages/daemon/src/llm.ts @@ -1,6 +1,9 @@ import OpenAI from "openai"; import type { IMessageLifecycle, ModelSettings } from "./types"; import Anthropic from "@anthropic-ai/sdk"; +import type { ChatCompletionMessageParam } from "openai/resources"; +import type { MessageParam } from "@anthropic-ai/sdk/resources"; +import { parseTemplate } from "./templateParser"; export const SYSTEM_PROMPT = ` You are an AI agent operating within a framework that provides you with: @@ -74,7 +77,8 @@ export async function generateEmbeddings( export async function generateText( generationModelSettings: ModelSettings, generationModelKey: string, - userMessage: string + userMessage: string, + customSystemPrompt?: string ): Promise { switch (generationModelSettings?.provider) { case "openai": @@ -89,7 +93,7 @@ export async function generateText( messages: [ { role: "system", - content: SYSTEM_PROMPT, + content: customSystemPrompt ?? SYSTEM_PROMPT, }, { role: "user", @@ -110,7 +114,7 @@ export async function generateText( const anthropicResponse = await anthropic.messages.create({ model: generationModelSettings.name, - system: SYSTEM_PROMPT, + system: customSystemPrompt ?? SYSTEM_PROMPT, messages: [ { role: "user", @@ -126,8 +130,96 @@ export async function generateText( } } -export function createPrompt(lifecycle: IMessageLifecycle): string { - return ` +/** + * Generates text using an OpenAI or Anthropic compatible model, using multiple messages. + * + * @param generationModelSettings The settings for the generation model. + * @param generationModelKey The API key for the generation model. + * @param messages An array of messages to generate text from. + * @param customSystemPrompt An optional custom system prompt to use for the generation. + * @returns The generated text as a string. + */ +export async function generateTextWithMessages( + generationModelSettings: ModelSettings, + generationModelKey: string, + messages: { role: string; content: string }[], + customSystemPrompt?: string +): Promise { + switch (generationModelSettings?.provider) { + case "openai": + const openai = new OpenAI({ + apiKey: generationModelKey, + baseURL: generationModelSettings.endpoint, + dangerouslyAllowBrowser: true, + }); + + const formattedMessages = messages.map( + (m) => + ({ + role: m.role, + content: m.content, + }) as ChatCompletionMessageParam + ); + + const openaiResponse = await openai.chat.completions.create({ + model: generationModelSettings.name, + messages: [ + { + role: "system", + content: customSystemPrompt ?? SYSTEM_PROMPT, + }, + ...formattedMessages, + ], + temperature: generationModelSettings.temperature, + max_completion_tokens: generationModelSettings.maxTokens, + }); + + return openaiResponse.choices[0].message.content ?? ""; + break; + case "anthropic": + const anthropic = new Anthropic({ + apiKey: generationModelKey, + baseURL: generationModelSettings.endpoint, + }); + + const anthropicResponse = await anthropic.messages.create({ + model: generationModelSettings.name, + system: customSystemPrompt ?? SYSTEM_PROMPT, + messages: [ + ...messages.map( + (m) => + ({ + role: m.role, + content: m.content, + }) as MessageParam + ), + ], + max_tokens: generationModelSettings.maxTokens ?? 1000, + temperature: generationModelSettings.temperature ?? 0.2, + }); + + return anthropicResponse.content.join("\n"); + break; + } +} + +export function createPrompt( + lifecycle: IMessageLifecycle, + overridePromptTemplate?: string +): string { + if (overridePromptTemplate) { + return parseTemplate( + { + lifecycle, + message: { + role: "user", + content: lifecycle.message as string, + }, + }, + overridePromptTemplate + ).content; + } else { + return ` # Name ${lifecycle.daemonName} @@ -143,4 +235,58 @@ export function createPrompt(lifecycle: IMessageLifecycle): string { # Tools ${lifecycle.tools?.join("\n")} `; + } +} + +export function createMultiplePrompts( + lifecycle: IMessageLifecycle, + messages: { + role: "user" | "assistant"; + content: string; + }[], + overridePromptTemplate?: string +): { + role: "user" | "assistant"; + content: string; +}[] { + if (overridePromptTemplate) { + return messages.map((m) => { + return parseTemplate( + { + lifecycle, + message: { role: m.role, content: m.content }, + }, + overridePromptTemplate + ); + }); + } else { + return messages.map((m) => { + if (m.role === "user") { + return { + role: "user", + content: ` + # Name + ${lifecycle.daemonName} + + # Identity Prompt + ${lifecycle.identityPrompt} + + # User Message + ${m.content} + + # Context + ${lifecycle.context?.join("\n")} + + # Tools + ${lifecycle.tools?.join("\n")} + `, + }; + } else { + return { + role: "assistant", + content: m.content, + }; + } + }); + } } diff --git a/packages/daemon/src/templateParser.ts b/packages/daemon/src/templateParser.ts new file mode 100644 index 0000000..6b7a3e6 --- /dev/null +++ b/packages/daemon/src/templateParser.ts @@ -0,0 +1,61 @@ +import type { IMessageLifecycle } from "./types"; + +const MESSAGE_ALLOWED_VARIABLES = [ + "name", + "identity", + "message", + "context", + "tools", +] as const; + +type TemplateData = { + lifecycle: IMessageLifecycle; + message: { role: "user" | "assistant"; content: string }; +}; + +type MessageAllowedVariable = (typeof MESSAGE_ALLOWED_VARIABLES)[number]; + +function validateTemplate(template: string): string[] { + const variablePattern = /\{\{(\w+)\}\}/g; + const matches = Array.from(template.matchAll(variablePattern)); + const usedVars = matches.map((match) => match[1]); + + const invalidVars = usedVars.filter( + (v) => !MESSAGE_ALLOWED_VARIABLES.includes(v as MessageAllowedVariable) + ); + + return invalidVars; +} + +export function parseTemplate( + data: TemplateData, + template: string +): { role: "user" | "assistant"; content: string } { + const invalidVars = validateTemplate(template); + if (invalidVars.length > 0) { + throw new Error( + `Template contains invalid variables: ${invalidVars.join(", ")}` + ); + } + + const variableMap = { + name: data.lifecycle.daemonName, + identity: data.lifecycle.identityPrompt, + message: data.message, + context: data.lifecycle.context?.join("\n") ?? "", + tools: data.lifecycle.tools?.join("\n") ?? "", + }; + + return { + role: data.message.role, + content: template.replace( + /\{\{(\w+)\}\}/g, + (_, variable: MessageAllowedVariable) => { + if (MESSAGE_ALLOWED_VARIABLES.includes(variable)) { + return variableMap[variable] as string; + } + return `{{${variable}}}`; + } + ), + }; +} diff --git a/packages/daemon/src/types.ts b/packages/daemon/src/types.ts index ebba9c7..43084db 100644 --- a/packages/daemon/src/types.ts +++ b/packages/daemon/src/types.ts @@ -20,7 +20,7 @@ export type IHookLog = any; export const ZMessageLifecycle = z.object({ daemonPubkey: z.string(), - message: z.string(), + message: z.string().or(z.array(z.string())), messageId: z.string(), createdAt: z.string(), approval: z.string(), @@ -29,7 +29,7 @@ export const ZMessageLifecycle = z.object({ identityPrompt: z.string().nullable(), context: z.array(z.string()).default([]), tools: z.array(z.string()).default([]), - generatedPrompt: z.string().default(""), + generatedPrompt: z.string().or(z.array(z.string())).default(""), output: z.string().default(""), hooks: z.array(ZHook).default([]), hooksLog: z.array(z.string()).default([]), From 8640ac8d5bb4388effc472bdc3d75164dc9ad408 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:32:07 -0500 Subject: [PATCH 2/4] fix: remove embedding key --- packages/daemon/src/daemon.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/daemon/src/daemon.ts b/packages/daemon/src/daemon.ts index 8deb357..ac4b0ad 100644 --- a/packages/daemon/src/daemon.ts +++ b/packages/daemon/src/daemon.ts @@ -72,8 +72,6 @@ export class Daemon implements IDaemon { ) { this.modelApiKeys = { generationKey: opts.modelApiKeys.generationKey, - embeddingKey: - opts.modelApiKeys.embeddingKey ?? opts.modelApiKeys.generationKey, }; this.keypair = opts.privateKey; @@ -515,7 +513,7 @@ export class Daemon implements IDaemon { throw new Error("Character not found"); } - if (!this.modelApiKeys.embeddingKey || !this.modelApiKeys.generationKey) { + if (!this.modelApiKeys.generationKey) { throw new Error("Model API keys not found"); } From bea2dc483a1a0aec0d0d65244c3234f8d2824eb6 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:30:44 -0500 Subject: [PATCH 3/4] chore: remove unused imports --- packages/daemon/src/daemon.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/daemon/src/daemon.ts b/packages/daemon/src/daemon.ts index ac4b0ad..8b475c0 100644 --- a/packages/daemon/src/daemon.ts +++ b/packages/daemon/src/daemon.ts @@ -14,7 +14,6 @@ import type { Keypair } from "@solana/web3.js"; import { createMultiplePrompts, createPrompt, - generateEmbeddings, generateText, generateTextWithMessages, } from "./llm.js"; From c043a88dd59e479ac6bd7a826d7a81d506dbfec2 Mon Sep 17 00:00:00 2001 From: Joey Meere <100378695+joeymeere@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:52:23 -0500 Subject: [PATCH 4/4] misc: add tests for prompt templates & multi-msg --- .env.sample | 6 +- bun.lockb | Bin 163864 -> 163792 bytes package.json | 5 +- packages/daemon/src/daemon.ts | 8 +- packages/daemon/src/llm.ts | 18 ++--- packages/daemon/src/templateParser.ts | 2 +- packages/daemon/src/types.ts | 5 ++ tests/e2e.test.ts | 104 ++++++++++++++++++++++++++ 8 files changed, 130 insertions(+), 18 deletions(-) create mode 100644 tests/e2e.test.ts diff --git a/.env.sample b/.env.sample index 9847a1d..d4f9581 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,5 @@ -OPENAI_API_KEY= \ No newline at end of file +OPENAI_API_KEY= +OPENAI_ENDPOINT= + +# For running the tests +IDENTITY_SERVER_URL= \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 1ee564d2169f101236d494ba2f3fab52ac0f5b33..36941d6c49c5ec1202f798d2f4b95d4ba76b7a94 100755 GIT binary patch delta 14462 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 delta 14579 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 { switch (generationModelSettings?.provider) { @@ -240,15 +244,9 @@ export function createPrompt( export function createMultiplePrompts( lifecycle: IMessageLifecycle, - messages: { - role: "user" | "assistant"; - content: string; - }[], + messages: MultiMessageSchema[], overridePromptTemplate?: string -): { - role: "user" | "assistant"; - content: string; -}[] { +): MultiMessageSchema[] { if (overridePromptTemplate) { return messages.map((m) => { return parseTemplate( diff --git a/packages/daemon/src/templateParser.ts b/packages/daemon/src/templateParser.ts index 6b7a3e6..f389785 100644 --- a/packages/daemon/src/templateParser.ts +++ b/packages/daemon/src/templateParser.ts @@ -41,7 +41,7 @@ export function parseTemplate( const variableMap = { name: data.lifecycle.daemonName, identity: data.lifecycle.identityPrompt, - message: data.message, + message: data.message.content, context: data.lifecycle.context?.join("\n") ?? "", tools: data.lifecycle.tools?.join("\n") ?? "", }; diff --git a/packages/daemon/src/types.ts b/packages/daemon/src/types.ts index 43084db..02b0ec5 100644 --- a/packages/daemon/src/types.ts +++ b/packages/daemon/src/types.ts @@ -127,6 +127,11 @@ export interface ToolRegistration { tool: ITool; } +export interface MultiMessageSchema { + role: "user" | "assistant"; + content: string; +} + export interface IDaemon { // Properties character: Character | undefined; diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts new file mode 100644 index 0000000..a659c02 --- /dev/null +++ b/tests/e2e.test.ts @@ -0,0 +1,104 @@ +import { + Daemon, + type Character, + type MultiMessageSchema, +} from "@spacemangaming/daemon"; +import { Keypair } from "@solana/web3.js"; +import { expect, test } from "bun:test"; + +test( + "Should use message templates & handle multiple messages", + async () => { + const daemon = new Daemon(); + + const identityKp = Keypair.generate(); + const prompt = + "You are Bob, a helpful assistant who has a knack for building things."; + + await daemon.init(process.env.IDENTITY_SERVER_URL!, { + character: { + name: "Bob", + pubkey: identityKp.publicKey.toBase58(), + identityPrompt: prompt, + identityServerUrl: process.env.IDENTITY_SERVER_URL!, + modelSettings: { + embedding: { + provider: "openai", + name: "text-embedding-3-small", + endpoint: process.env.OPENAI_ENDPOINT!, + apiKey: process.env.OPENAI_API_KEY!, + }, + generation: { + provider: "openai", + name: "gpt-4o", + endpoint: process.env.OPENAI_ENDPOINT!, + apiKey: process.env.OPENAI_API_KEY!, + }, + }, + bootstrap: [], + } as Character, + privateKey: identityKp, + modelApiKeys: { + generationKey: process.env.OPENAI_API_KEY!, + embeddingKey: process.env.OPENAI_API_KEY!, + }, + }); + + const lifecycle = await daemon.message("Hello!", { + context: true, + actions: true, + postProcess: false, + customSystemPrompt: + "As Bob The Builder, guide the user through a conversation.", + customMessageTemplate: ` + {{message}} + `, + }); + + expect(lifecycle).toBeDefined(); + + console.log("Message:", lifecycle.message); + console.log("Output:", lifecycle.output); + console.log("Generated Prompts:", lifecycle.generatedPrompt); + + const historyWithNewMessage: MultiMessageSchema[] = [ + { + role: "user", + content: lifecycle.message as string, + }, + { + role: "assistant", + content: lifecycle.output, + }, + { + role: "user", + content: "Awesome!", + }, + ]; + + console.log("History with new message:", historyWithNewMessage); + + const lifecycleWithMultiple = await daemon.multipleMessages( + historyWithNewMessage, + { + context: true, + actions: true, + postProcess: false, + customSystemPrompt: + "As Bob The Builder, guide the user through a conversation.", + customMessageTemplate: ` + {{message}} + `, + } + ); + + expect(lifecycleWithMultiple).toBeDefined(); + + const generatedPrompts = lifecycleWithMultiple.generatedPrompt as string[]; + + console.log("Messages:", lifecycleWithMultiple.message); + console.log("Output:", lifecycleWithMultiple.output); + console.log("Generated Prompts:", generatedPrompts); + }, + { timeout: 60000 } +);