create-message-kit 1.0.20 → 1.1.5-beta.1

Sign up to get free protection for your applications and to get access to all the features.
package/index.js CHANGED
@@ -9,27 +9,21 @@ import pc from "picocolors";
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
 
12
- // Read package.json to get the version
13
- const packageJson = JSON.parse(
14
- fs.readFileSync(resolve(__dirname, "package.json"), "utf8"),
15
- );
16
- const version = packageJson.version;
17
-
18
12
  program
19
13
  .name("byob")
20
14
  .description("CLI to initialize projects")
21
15
  .action(async () => {
22
- log.info(pc.cyan(`Welcome to MessageKit v${version}!`));
16
+ log.info(pc.cyan(`Welcome to MessageKit!`));
23
17
  const coolLogo = `
24
- ███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
25
- ████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
26
- ██╔████╔██║█████╗ ███████╗███████╗███████║██║ ███╗█████╗ █████╔╝ ██║ ██║
27
- ██║╚██╔╝██║██╔══╝ ╚════██║╚════██║██╔══██║██║ ██║██╔══╝ ██╔═██╗ ██║ ██║
28
- ██║ ╚═╝ ██║███████╗███████║███████║██║ ██║╚██████╔╝███████╗██║ ██╗██║ ██║
29
- ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
30
- Powered by XMTP`;
18
+ ███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
19
+ ████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
20
+ ██╔████╔██║█████╗ ███████╗███████╗███████║██║ ███╗█████╗ █████╔╝ ██║ ██║
21
+ ██║╚██╔╝██║██╔══╝ ╚════██║╚════██║██╔══██║██║ ██║██╔══╝ ██╔═██╗ ██║ ██║
22
+ ██║ ╚═╝ ██║███████╗███████║███████║██║ ██║╚██████╔╝███████╗██║ ██╗██║ ██║
23
+ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
24
+ Powered by XMTP`;
31
25
 
32
- log.info(pc.cyan(coolLogo));
26
+ log.info(pc.red(coolLogo));
33
27
 
34
28
  const { templateType, displayName, destDir } = await gatherProjectInfo();
35
29
 
@@ -54,7 +48,7 @@ program
54
48
  const pkgManager = detectPackageManager();
55
49
 
56
50
  // Create README.md file
57
- createReadme(destDir, templateType, displayName, pkgManager);
51
+ createReadme(destDir, displayName, pkgManager);
58
52
 
59
53
  // Log next steps
60
54
  logNextSteps(displayName);
@@ -163,12 +157,21 @@ function replaceDotfiles(destDir) {
163
157
  }
164
158
 
165
159
  function updatePackageJson(destDir, name) {
166
- const pkgJson = fs.readJsonSync(resolve(destDir, "package.json"));
167
- pkgJson.name = name;
168
- updateDependenciesToLatest(pkgJson);
169
- fs.writeJsonSync(resolve(destDir, "package.json"), pkgJson, { spaces: 2 });
170
- }
160
+ try {
161
+ const pkgJsonPath = resolve(destDir, "package.json");
162
+ if (!fs.existsSync(pkgJsonPath)) {
163
+ log.error(`package.json not found in ${pkgJsonPath}`);
164
+ return;
165
+ }
171
166
 
167
+ const pkgJson = fs.readJsonSync(pkgJsonPath);
168
+ pkgJson.name = name;
169
+ updateDependenciesToLatest(pkgJson);
170
+ fs.writeJsonSync(pkgJsonPath, pkgJson, { spaces: 2 });
171
+ } catch (error) {
172
+ log.error(`Error updating package.json: ${error.message}`);
173
+ }
174
+ }
172
175
  function logNextSteps(name) {
173
176
  log.message("Next steps:");
174
177
  log.step(`1. ${pc.red(`cd ./${name}`)} - Navigate to project`);
@@ -226,7 +229,7 @@ function createEnvFile(destDir) {
226
229
  }
227
230
 
228
231
  // Create README.md file
229
- function createReadme(destDir, templateType, projectName, packageManager) {
232
+ function createReadme(destDir, projectName, packageManager) {
230
233
  const envExampleContent = fs.readFileSync(
231
234
  resolve(destDir, ".env.example"),
232
235
  "utf8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-message-kit",
3
- "version": "1.0.20",
3
+ "version": "1.1.5-beta.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,10 +1,7 @@
1
1
  import { HandlerContext } from "@xmtp/message-kit";
2
2
  import { getUserInfo, clearInfoCache, isOnXMTP } from "../lib/resolver.js";
3
- import { textGeneration } from "../lib/openai.js";
4
- import { processResponseWithSkill } from "../lib/openai.js";
5
3
  import { isAddress } from "viem";
6
- import { ens_agent_prompt } from "../prompt.js";
7
- import { clearChatHistories } from "../lib/openai.js";
4
+ import { clearMemory } from "../lib/openai.js";
8
5
 
9
6
  export const frameUrl = "https://ens.steer.fun/";
10
7
  export const ensUrl = "https://app.ens.domains/";
@@ -17,7 +14,7 @@ export async function handleEns(context: HandlerContext) {
17
14
  },
18
15
  } = context;
19
16
  if (command == "reset") {
20
- clear();
17
+ clearMemory();
21
18
  return { code: 200, message: "Conversation reset." };
22
19
  } else if (command == "renew") {
23
20
  // Destructure and validate parameters for the ens command
@@ -32,7 +29,7 @@ export async function handleEns(context: HandlerContext) {
32
29
 
33
30
  const data = await getUserInfo(domain);
34
31
 
35
- if (!data || data?.address !== sender?.address) {
32
+ if (!data?.address || data?.address !== sender?.address) {
36
33
  return {
37
34
  code: 403,
38
35
  message:
@@ -60,7 +57,7 @@ export async function handleEns(context: HandlerContext) {
60
57
  const { domain } = params;
61
58
 
62
59
  const data = await getUserInfo(domain);
63
- if (!data) {
60
+ if (!data?.ensDomain) {
64
61
  return {
65
62
  code: 404,
66
63
  message: "Domain not found.",
@@ -151,39 +148,6 @@ export async function handleEns(context: HandlerContext) {
151
148
  }
152
149
  }
153
150
 
154
- export async function ensAgent(context: HandlerContext) {
155
- if (!process?.env?.OPEN_AI_API_KEY) {
156
- console.warn("No OPEN_AI_API_KEY found in .env");
157
- return;
158
- }
159
-
160
- const {
161
- message: {
162
- content: { content, params },
163
- sender,
164
- },
165
- group,
166
- } = context;
167
-
168
- try {
169
- let userPrompt = params?.prompt ?? content;
170
- const userInfo = await getUserInfo(sender.address);
171
- if (!userInfo) {
172
- console.log("User info not found");
173
- return;
174
- }
175
- const { reply } = await textGeneration(
176
- sender.address,
177
- userPrompt,
178
- await ens_agent_prompt(userInfo),
179
- );
180
- await processResponseWithSkill(sender.address, reply, context);
181
- } catch (error) {
182
- console.error("Error during OpenAI call:", error);
183
- await context.send("An error occurred while processing your request.");
184
- }
185
- }
186
-
187
151
  export const generateCoolAlternatives = (domain: string) => {
188
152
  const suffixes = ["lfg", "cool", "degen", "moon", "base", "gm"];
189
153
  const alternatives = [];
@@ -206,6 +170,6 @@ export const generateCoolAlternatives = (domain: string) => {
206
170
  };
207
171
 
208
172
  export async function clear() {
209
- clearChatHistories();
173
+ clearMemory();
210
174
  clearInfoCache();
211
175
  }
@@ -1,10 +1,39 @@
1
1
  import { run, HandlerContext } from "@xmtp/message-kit";
2
- import { ensAgent } from "./handler/ens.js";
2
+ import { textGeneration, processResponseWithSkill } from "./lib/openai.js";
3
+ import { agent_prompt } from "./prompt.js";
4
+ import { getUserInfo } from "./lib/resolver.js";
3
5
 
4
6
  run(async (context: HandlerContext) => {
5
7
  /*All the commands are handled through the commands file*/
6
8
  /* If its just text, it will be handled by the ensAgent*/
7
9
  /* If its a group message, it will be handled by the groupAgent*/
10
+ if (!process?.env?.OPEN_AI_API_KEY) {
11
+ console.warn("No OPEN_AI_API_KEY found in .env");
12
+ return;
13
+ }
8
14
 
9
- ensAgent(context);
15
+ const {
16
+ message: {
17
+ content: { content, params },
18
+ sender,
19
+ },
20
+ } = context;
21
+
22
+ try {
23
+ let userPrompt = params?.prompt ?? content;
24
+ const userInfo = await getUserInfo(sender.address);
25
+ if (!userInfo) {
26
+ console.log("User info not found");
27
+ return;
28
+ }
29
+ const { reply } = await textGeneration(
30
+ sender.address,
31
+ userPrompt,
32
+ await agent_prompt(userInfo),
33
+ );
34
+ await processResponseWithSkill(sender.address, reply, context);
35
+ } catch (error) {
36
+ console.error("Error during OpenAI call:", error);
37
+ await context.send("An error occurred while processing your request.");
38
+ }
10
39
  });
@@ -1,6 +1,6 @@
1
1
  import dotenv from "dotenv";
2
2
  dotenv.config();
3
-
3
+ import type { SkillGroup } from "@xmtp/message-kit";
4
4
  import OpenAI from "openai";
5
5
  const openai = new OpenAI({
6
6
  apiKey: process.env.OPEN_AI_API_KEY,
@@ -9,13 +9,91 @@ const openai = new OpenAI({
9
9
  export type ChatHistoryEntry = { role: string; content: string };
10
10
  export type ChatHistories = Record<string, ChatHistoryEntry[]>;
11
11
 
12
+ // New ChatMemory class
13
+ class ChatMemory {
14
+ private histories: ChatHistories = {};
15
+
16
+ getHistory(address: string): ChatHistoryEntry[] {
17
+ return this.histories[address] || [];
18
+ }
19
+
20
+ addEntry(address: string, entry: ChatHistoryEntry) {
21
+ if (!this.histories[address]) {
22
+ this.histories[address] = [];
23
+ }
24
+ this.histories[address].push(entry);
25
+ }
26
+
27
+ initializeWithSystem(address: string, systemPrompt: string) {
28
+ if (this.getHistory(address).length === 0) {
29
+ this.addEntry(address, {
30
+ role: "system",
31
+ content: systemPrompt,
32
+ });
33
+ }
34
+ }
35
+
36
+ clear() {
37
+ this.histories = {};
38
+ }
39
+ }
40
+
41
+ export const clearMemory = () => {
42
+ chatHistories = {};
43
+ };
44
+
45
+ // Create singleton instance
46
+ export const chatMemory = new ChatMemory();
47
+
12
48
  let chatHistories: ChatHistories = {};
49
+ export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
50
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
51
+ - You can trigger commands by only sending the command in a newline message.
52
+ - Never announce actions without using a command separated by a newline character.
53
+ - Dont answer in markdown format, just answer in plaintext.
54
+ - Do not make guesses or assumptions
55
+ - Only answer if the verified information is in the prompt.
56
+ - Check that you are not missing a command
57
+ - Focus only on helping users with operations detailed below.
58
+ `;
59
+
60
+ export const PROMPT_SKILLS_AND_EXAMPLES = (skills: SkillGroup[]) => `
61
+ Commands:
62
+ ${skills
63
+ .map((skill) => skill.skills.map((s) => s.command).join("\n"))
64
+ .join("\n")}
65
+
66
+ Examples:
67
+ ${skills
68
+ .map((skill) => skill.skills.map((s) => s.examples).join("\n"))
69
+ .join("\n")}
70
+ `;
71
+
72
+ export async function agentResponse(
73
+ sender: { address: string },
74
+ userPrompt: string,
75
+ systemPrompt: string,
76
+ context: any,
77
+ ) {
78
+ try {
79
+ const { reply } = await textGeneration(
80
+ sender.address,
81
+ userPrompt,
82
+ systemPrompt,
83
+ );
84
+ await processResponseWithSkill(sender.address, reply, context);
85
+ } catch (error) {
86
+ console.error("Error during OpenAI call:", error);
87
+ await context.reply("An error occurred while processing your request.");
88
+ }
89
+ }
13
90
  export async function textGeneration(
14
91
  address: string,
15
92
  userPrompt: string,
16
93
  systemPrompt: string,
17
94
  ) {
18
- let messages = chatHistories[address] || [];
95
+ let messages = chatMemory.getHistory(address);
96
+ chatMemory.initializeWithSystem(address, systemPrompt);
19
97
  if (messages.length === 0) {
20
98
  messages.push({
21
99
  role: "system",
@@ -36,9 +114,11 @@ export async function textGeneration(
36
114
  role: "assistant",
37
115
  content: reply || "No response from OpenAI.",
38
116
  });
39
- const cleanedReply = responseParser(reply as string);
40
- chatHistories[address] = messages;
41
- console.log("messages.length", messages.length);
117
+ const cleanedReply = parseMarkdown(reply as string);
118
+ chatMemory.addEntry(address, {
119
+ role: "assistant",
120
+ content: cleanedReply,
121
+ });
42
122
  return { reply: cleanedReply, history: messages };
43
123
  } catch (error) {
44
124
  console.error("Failed to fetch from OpenAI:", error);
@@ -46,43 +126,6 @@ export async function textGeneration(
46
126
  }
47
127
  }
48
128
 
49
- // New method to interpret an image
50
- export async function vision(imageData: Uint8Array, systemPrompt: string) {
51
- const base64Image = Buffer.from(imageData).toString("base64");
52
- const dataUrl = `data:image/jpeg;base64,${base64Image}`;
53
-
54
- // Create a new thread for each vision request
55
- const visionMessages = [
56
- {
57
- role: "system",
58
- content: systemPrompt,
59
- },
60
- {
61
- role: "user",
62
- content: [
63
- { type: "text", text: systemPrompt },
64
- {
65
- type: "image_url",
66
- image_url: {
67
- url: dataUrl,
68
- },
69
- },
70
- ],
71
- },
72
- ];
73
-
74
- try {
75
- const response = await openai.chat.completions.create({
76
- model: "gpt-4o",
77
- messages: visionMessages as any,
78
- });
79
- return response.choices[0].message.content;
80
- } catch (error) {
81
- console.error("Failed to interpret image with OpenAI:", error);
82
- throw error;
83
- }
84
- }
85
-
86
129
  export async function processResponseWithSkill(
87
130
  address: string,
88
131
  reply: string,
@@ -90,24 +133,19 @@ export async function processResponseWithSkill(
90
133
  ) {
91
134
  let messages = reply
92
135
  .split("\n")
93
- .map((message: string) => responseParser(message))
136
+ .map((message: string) => parseMarkdown(message))
94
137
  .filter((message): message is string => message.length > 0);
95
138
 
96
139
  console.log(messages);
97
140
  for (const message of messages) {
98
141
  if (message.startsWith("/")) {
99
142
  const response = await context.skill(message);
100
- if (response && response.message) {
101
- let msg = responseParser(response.message);
102
-
103
- if (!chatHistories[address]) {
104
- chatHistories[address] = [];
105
- }
106
- chatHistories[address].push({
143
+ if (response && typeof response.message === "string") {
144
+ let msg = parseMarkdown(response.message);
145
+ chatMemory.addEntry(address, {
107
146
  role: "system",
108
147
  content: msg,
109
148
  });
110
-
111
149
  await context.send(response.message);
112
150
  }
113
151
  } else {
@@ -115,7 +153,7 @@ export async function processResponseWithSkill(
115
153
  }
116
154
  }
117
155
  }
118
- export function responseParser(message: string) {
156
+ export function parseMarkdown(message: string) {
119
157
  let trimmedMessage = message;
120
158
  // Remove bold and underline markdown
121
159
  trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
@@ -134,7 +172,3 @@ export function responseParser(message: string) {
134
172
 
135
173
  return trimmedMessage;
136
174
  }
137
-
138
- export const clearChatHistories = () => {
139
- chatHistories = {};
140
- };
@@ -17,6 +17,7 @@ export type UserInfo = {
17
17
  ensDomain?: string | undefined;
18
18
  address?: string | undefined;
19
19
  converseUsername?: string | undefined;
20
+ preferredName?: string | undefined;
20
21
  ensInfo?: EnsData | undefined;
21
22
  avatar?: string | undefined;
22
23
  };
@@ -54,12 +55,11 @@ export const getUserInfo = async (
54
55
  converseUsername: undefined,
55
56
  ensInfo: undefined,
56
57
  };
57
- //console.log("Getting user info", key, clientAddress);
58
58
  if (isAddress(clientAddress || "")) {
59
59
  data.address = clientAddress;
60
60
  } else if (isAddress(key || "")) {
61
61
  data.address = key;
62
- } else if (key.includes(".eth")) {
62
+ } else if (key?.includes(".eth")) {
63
63
  data.ensDomain = key;
64
64
  } else if (key == "@user" || key == "@me" || key == "@bot") {
65
65
  data.address = clientAddress;
@@ -77,12 +77,8 @@ export const getUserInfo = async (
77
77
 
78
78
  let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
79
  let cacheData = keyToUse && infoCache.get(keyToUse);
80
- if (cacheData) {
81
- //console.log("Getting user info", keyToUse, cacheData);
82
- return cacheData;
83
- } else {
84
- //console.log("Getting user info", keyToUse, data);
85
- }
80
+ console.log("Getting user info", { cacheData, keyToUse, data });
81
+ if (cacheData) return cacheData;
86
82
 
87
83
  if (keyToUse?.includes(".eth")) {
88
84
  const response = await fetch(`https://ensdata.net/${keyToUse}`);
@@ -124,3 +120,17 @@ export const isOnXMTP = async (
124
120
  if (domain == "fabri.eth") return false;
125
121
  if (address) return (await client.canMessage([address])).length > 0;
126
122
  };
123
+
124
+ export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
125
+ let { address, ensDomain, converseUsername, preferredName } = userInfo;
126
+ let prompt = `User context:
127
+ - Start by fetch their domain from or Convese username
128
+ - Call the user by their name or domain, in case they have one
129
+ - Ask for a name (if they don't have one) so you can suggest domains.
130
+ - Users address is: ${address}`;
131
+ if (preferredName) prompt += `\n- Users name is: ${preferredName}`;
132
+ if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
133
+ if (converseUsername)
134
+ prompt += `\n- Converse username is: ${converseUsername}`;
135
+ return prompt;
136
+ };
@@ -1,42 +1,28 @@
1
1
  import { skills } from "./skills.js";
2
- import type { UserInfo } from "./lib/resolver.js";
2
+ import { UserInfo, PROMPT_USER_CONTENT } from "./lib/resolver.js";
3
+ import { PROMPT_RULES, PROMPT_SKILLS_AND_EXAMPLES } from "./lib/openai.js";
3
4
 
4
- export async function ens_agent_prompt(userInfo: UserInfo) {
5
- let { address, ensDomain, converseUsername } = userInfo;
5
+ export async function agent_prompt(userInfo: UserInfo) {
6
+ let { address, ensDomain, converseUsername, preferredName } = userInfo;
6
7
 
7
- const systemPrompt = `You are a helpful and playful agent called @ens that lives inside a web3 messaging app called Converse.
8
- - You can respond with multiple messages if needed. Each message should be separated by a newline character.
9
- - You can trigger commands by only sending the command in a newline message.
10
- - Never announce actions without using a command separated by a newline character.
11
- - Only provide answers based on verified information.
12
- - Dont answer in markdown format, just answer in plaintext.
13
- - Do not make guesses or assumptions
14
- - CHECK that you are not missing a command
8
+ //Update the name of the agent with predefined prompt
9
+ let systemPrompt = PROMPT_RULES.replace("{NAME}", skills?.[0]?.tag ?? "@ens");
15
10
 
16
- User context:
17
- - Users address is: ${address}
18
- ${ensDomain != undefined ? `- User ENS domain is: ${ensDomain}` : ""}
19
- ${converseUsername != undefined ? `- Converse username is: ${converseUsername}` : ""}
11
+ //Add user context to the prompt
12
+ systemPrompt += PROMPT_USER_CONTENT(userInfo);
20
13
 
21
- ## Task
22
- - Start by fetch their domain from or Convese username
23
- - Call the user by their name or domain, in case they have one
24
- - Ask for a name (if they don't have one) so you can suggest domains.
25
-
26
- Commands:
27
- ${skills.map((skill) => skill.skills.map((s) => s.command).join("\n")).join("\n")}
28
-
29
- Examples:
30
- ${skills.map((skill) => skill.skills.map((s) => s.example).join("\n")).join("\n")}
14
+ //Add skills and examples to the prompt
15
+ systemPrompt += PROMPT_SKILLS_AND_EXAMPLES(skills);
31
16
 
17
+ systemPrompt += `
32
18
 
33
19
  ## Example responses:
34
20
 
35
21
  1. Check if the user does not have a ENS domain
36
- Hey ${converseUsername}! it looks like you don't have a ENS domain yet! \n\Let me start by checking your Converse username with the .eth suffix\n/check ${converseUsername}.eth
22
+ Hey ${preferredName}! it looks like you don't have a ENS domain yet! \n\Let me start by checking your Converse username with the .eth suffix\n/check ${converseUsername}.eth
37
23
 
38
24
  2. If the user has a ENS domain
39
- Hello ${ensDomain} ! I'll help you get your ENS domain.\n Let's start by checking your ENS domain ${ensDomain}. Give me a moment.\n/check ${ensDomain}
25
+ Hello ${preferredName} ! I'll help you get your ENS domain.\n Let's start by checking your ENS domain ${ensDomain}. Give me a moment.\n/check ${ensDomain}
40
26
 
41
27
  3. Check if the ENS domain is available
42
28
  Hello! I'll help you get your domain.\n Let's start by checking your ENS domain ${ensDomain}. Give me a moment.\n/check ${ensDomain}
@@ -62,8 +48,9 @@ ${skills.map((skill) => skill.skills.map((s) => s.example).join("\n")).join("\n"
62
48
  10. If the user wants cool suggestions about a domain, use the command "/cool [domain]"
63
49
  Here are some cool suggestions for your domain.\n/cool ${ensDomain}
64
50
 
65
- ## Most common bug
66
- Some times you will say something like: "Looks like vitalik.eth is registered! What about these cool alternatives?"
51
+ ## Most common bugs
52
+
53
+ 1. Some times you will say something like: "Looks like vitalik.eth is registered! What about these cool alternatives?"
67
54
  But you forgot to add the command at the end of the message.
68
55
  You should have said something like: "Looks like vitalik.eth is registered! What about these cool alternatives?\n/cool vitalik.eth
69
56
  `;
@@ -1,11 +1,10 @@
1
- import { handleEns, ensAgent } from "./handler/ens.js";
1
+ import { handleEns } from "./handler/ens.js";
2
2
  import type { SkillGroup } from "@xmtp/message-kit";
3
3
 
4
4
  export const skills: SkillGroup[] = [
5
5
  {
6
6
  name: "Ens Domain Bot",
7
7
  tag: "@ens",
8
- tagHandler: ensAgent,
9
8
  description: "Register ENS domains.",
10
9
  skills: [
11
10
  {
@@ -14,7 +13,7 @@ export const skills: SkillGroup[] = [
14
13
  handler: handleEns,
15
14
  description:
16
15
  "Register a new ENS domain. Returns a URL to complete the registration process.",
17
- example: "/register vitalik.eth",
16
+ examples: ["/register vitalik.eth"],
18
17
  params: {
19
18
  domain: {
20
19
  type: "string",
@@ -27,7 +26,7 @@ export const skills: SkillGroup[] = [
27
26
  handler: handleEns,
28
27
  description:
29
28
  "Get detailed information about an ENS domain including owner, expiry date, and resolver.",
30
- example: "/info nick.eth",
29
+ examples: ["/info nick.eth"],
31
30
  params: {
32
31
  domain: {
33
32
  type: "string",
@@ -40,7 +39,7 @@ export const skills: SkillGroup[] = [
40
39
  handler: handleEns,
41
40
  description:
42
41
  "Extend the registration period of your ENS domain. Returns a URL to complete the renewal.",
43
- example: "/renew fabri.base.eth",
42
+ examples: ["/renew fabri.base.eth"],
44
43
  params: {
45
44
  domain: {
46
45
  type: "string",
@@ -48,22 +47,21 @@ export const skills: SkillGroup[] = [
48
47
  },
49
48
  },
50
49
  {
51
- command: "/check [domain] [cool_alternatives]",
50
+ command: "/check [domain]",
52
51
  triggers: ["/check"],
53
52
  handler: handleEns,
53
+ examples: ["/check vitalik.eth", "/check fabri.base.eth"],
54
54
  description: "Check if a domain is available.",
55
55
  params: {
56
56
  domain: {
57
57
  type: "string",
58
58
  },
59
- cool_alternatives: {
60
- type: "quoted",
61
- },
62
59
  },
63
60
  },
64
61
  {
65
62
  command: "/cool [domain]",
66
63
  triggers: ["/cool"],
64
+ examples: ["/cool vitalik.eth"],
67
65
  handler: handleEns,
68
66
  description: "Get cool alternatives for a .eth domain.",
69
67
  params: {
@@ -75,6 +73,7 @@ export const skills: SkillGroup[] = [
75
73
  {
76
74
  command: "/reset",
77
75
  triggers: ["/reset"],
76
+ examples: ["/reset"],
78
77
  handler: handleEns,
79
78
  description: "Reset the conversation.",
80
79
  params: {},
@@ -84,6 +83,7 @@ export const skills: SkillGroup[] = [
84
83
  description: "Show a URL for tipping a domain owner.",
85
84
  triggers: ["/tip"],
86
85
  handler: handleEns,
86
+ examples: ["/tip 0x1234567890123456789012345678901234567890"],
87
87
  params: {
88
88
  address: {
89
89
  type: "string",
@@ -1,5 +1,6 @@
1
1
  import { HandlerContext } from "@xmtp/message-kit";
2
- import { vision, textGeneration } from "../lib/openai.js";
2
+ import { textGeneration } from "../lib/openai.js";
3
+ import { vision } from "../lib/vision.js";
3
4
  import { getUserInfo } from "../lib/resolver.js";
4
5
 
5
6
  export async function handler(context: HandlerContext) {
@@ -52,12 +53,7 @@ export async function handler(context: HandlerContext) {
52
53
  `;
53
54
 
54
55
  //I want the reply to be an array of messages so the bot feels like is sending multuple ones
55
- const { reply } = await textGeneration(
56
- sender.address,
57
- response,
58
- prompt,
59
- true,
60
- );
56
+ const { reply } = await textGeneration(sender.address, response, prompt);
61
57
  let splitMessages = JSON.parse(reply);
62
58
  for (const message of splitMessages) {
63
59
  let msg = message as string;
@@ -1,6 +1,6 @@
1
1
  import dotenv from "dotenv";
2
2
  dotenv.config();
3
-
3
+ import type { SkillGroup } from "@xmtp/message-kit";
4
4
  import OpenAI from "openai";
5
5
  const openai = new OpenAI({
6
6
  apiKey: process.env.OPEN_AI_API_KEY,
@@ -9,13 +9,91 @@ const openai = new OpenAI({
9
9
  export type ChatHistoryEntry = { role: string; content: string };
10
10
  export type ChatHistories = Record<string, ChatHistoryEntry[]>;
11
11
 
12
+ // New ChatMemory class
13
+ class ChatMemory {
14
+ private histories: ChatHistories = {};
15
+
16
+ getHistory(address: string): ChatHistoryEntry[] {
17
+ return this.histories[address] || [];
18
+ }
19
+
20
+ addEntry(address: string, entry: ChatHistoryEntry) {
21
+ if (!this.histories[address]) {
22
+ this.histories[address] = [];
23
+ }
24
+ this.histories[address].push(entry);
25
+ }
26
+
27
+ initializeWithSystem(address: string, systemPrompt: string) {
28
+ if (this.getHistory(address).length === 0) {
29
+ this.addEntry(address, {
30
+ role: "system",
31
+ content: systemPrompt,
32
+ });
33
+ }
34
+ }
35
+
36
+ clear() {
37
+ this.histories = {};
38
+ }
39
+ }
40
+
41
+ export const clearMemory = () => {
42
+ chatHistories = {};
43
+ };
44
+
45
+ // Create singleton instance
46
+ export const chatMemory = new ChatMemory();
47
+
12
48
  let chatHistories: ChatHistories = {};
49
+ export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
50
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
51
+ - You can trigger commands by only sending the command in a newline message.
52
+ - Never announce actions without using a command separated by a newline character.
53
+ - Dont answer in markdown format, just answer in plaintext.
54
+ - Do not make guesses or assumptions
55
+ - Only answer if the verified information is in the prompt.
56
+ - Check that you are not missing a command
57
+ - Focus only on helping users with operations detailed below.
58
+ `;
59
+
60
+ export const PROMPT_SKILLS_AND_EXAMPLES = (skills: SkillGroup[]) => `
61
+ Commands:
62
+ ${skills
63
+ .map((skill) => skill.skills.map((s) => s.command).join("\n"))
64
+ .join("\n")}
65
+
66
+ Examples:
67
+ ${skills
68
+ .map((skill) => skill.skills.map((s) => s.examples).join("\n"))
69
+ .join("\n")}
70
+ `;
71
+
72
+ export async function agentResponse(
73
+ sender: { address: string },
74
+ userPrompt: string,
75
+ systemPrompt: string,
76
+ context: any,
77
+ ) {
78
+ try {
79
+ const { reply } = await textGeneration(
80
+ sender.address,
81
+ userPrompt,
82
+ systemPrompt,
83
+ );
84
+ await processResponseWithSkill(sender.address, reply, context);
85
+ } catch (error) {
86
+ console.error("Error during OpenAI call:", error);
87
+ await context.reply("An error occurred while processing your request.");
88
+ }
89
+ }
13
90
  export async function textGeneration(
14
91
  address: string,
15
92
  userPrompt: string,
16
93
  systemPrompt: string,
17
94
  ) {
18
- let messages = chatHistories[address] || [];
95
+ let messages = chatMemory.getHistory(address);
96
+ chatMemory.initializeWithSystem(address, systemPrompt);
19
97
  if (messages.length === 0) {
20
98
  messages.push({
21
99
  role: "system",
@@ -36,8 +114,11 @@ export async function textGeneration(
36
114
  role: "assistant",
37
115
  content: reply || "No response from OpenAI.",
38
116
  });
39
- const cleanedReply = responseParser(reply as string);
40
- chatHistories[address] = messages;
117
+ const cleanedReply = parseMarkdown(reply as string);
118
+ chatMemory.addEntry(address, {
119
+ role: "assistant",
120
+ content: cleanedReply,
121
+ });
41
122
  return { reply: cleanedReply, history: messages };
42
123
  } catch (error) {
43
124
  console.error("Failed to fetch from OpenAI:", error);
@@ -45,43 +126,6 @@ export async function textGeneration(
45
126
  }
46
127
  }
47
128
 
48
- // New method to interpret an image
49
- export async function vision(imageData: Uint8Array, systemPrompt: string) {
50
- const base64Image = Buffer.from(imageData).toString("base64");
51
- const dataUrl = `data:image/jpeg;base64,${base64Image}`;
52
-
53
- // Create a new thread for each vision request
54
- const visionMessages = [
55
- {
56
- role: "system",
57
- content: systemPrompt,
58
- },
59
- {
60
- role: "user",
61
- content: [
62
- { type: "text", text: systemPrompt },
63
- {
64
- type: "image_url",
65
- image_url: {
66
- url: dataUrl,
67
- },
68
- },
69
- ],
70
- },
71
- ];
72
-
73
- try {
74
- const response = await openai.chat.completions.create({
75
- model: "gpt-4o",
76
- messages: visionMessages as any,
77
- });
78
- return response.choices[0].message.content;
79
- } catch (error) {
80
- console.error("Failed to interpret image with OpenAI:", error);
81
- throw error;
82
- }
83
- }
84
-
85
129
  export async function processResponseWithSkill(
86
130
  address: string,
87
131
  reply: string,
@@ -89,24 +133,19 @@ export async function processResponseWithSkill(
89
133
  ) {
90
134
  let messages = reply
91
135
  .split("\n")
92
- .map((message: string) => responseParser(message))
136
+ .map((message: string) => parseMarkdown(message))
93
137
  .filter((message): message is string => message.length > 0);
94
138
 
95
139
  console.log(messages);
96
140
  for (const message of messages) {
97
141
  if (message.startsWith("/")) {
98
142
  const response = await context.skill(message);
99
- if (response && response.message) {
100
- let msg = responseParser(response.message);
101
-
102
- if (!chatHistories[address]) {
103
- chatHistories[address] = [];
104
- }
105
- chatHistories[address].push({
143
+ if (response && typeof response.message === "string") {
144
+ let msg = parseMarkdown(response.message);
145
+ chatMemory.addEntry(address, {
106
146
  role: "system",
107
147
  content: msg,
108
148
  });
109
-
110
149
  await context.send(response.message);
111
150
  }
112
151
  } else {
@@ -114,7 +153,7 @@ export async function processResponseWithSkill(
114
153
  }
115
154
  }
116
155
  }
117
- export function responseParser(message: string) {
156
+ export function parseMarkdown(message: string) {
118
157
  let trimmedMessage = message;
119
158
  // Remove bold and underline markdown
120
159
  trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
@@ -133,7 +172,3 @@ export function responseParser(message: string) {
133
172
 
134
173
  return trimmedMessage;
135
174
  }
136
-
137
- export const clearChatHistories = () => {
138
- chatHistories = {};
139
- };
@@ -17,6 +17,7 @@ export type UserInfo = {
17
17
  ensDomain?: string | undefined;
18
18
  address?: string | undefined;
19
19
  converseUsername?: string | undefined;
20
+ preferredName?: string | undefined;
20
21
  ensInfo?: EnsData | undefined;
21
22
  avatar?: string | undefined;
22
23
  };
@@ -54,12 +55,11 @@ export const getUserInfo = async (
54
55
  converseUsername: undefined,
55
56
  ensInfo: undefined,
56
57
  };
57
- //console.log("Getting user info", key, clientAddress);
58
58
  if (isAddress(clientAddress || "")) {
59
59
  data.address = clientAddress;
60
60
  } else if (isAddress(key || "")) {
61
61
  data.address = key;
62
- } else if (key.includes(".eth")) {
62
+ } else if (key?.includes(".eth")) {
63
63
  data.ensDomain = key;
64
64
  } else if (key == "@user" || key == "@me" || key == "@bot") {
65
65
  data.address = clientAddress;
@@ -77,12 +77,8 @@ export const getUserInfo = async (
77
77
 
78
78
  let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
79
  let cacheData = keyToUse && infoCache.get(keyToUse);
80
- if (cacheData) {
81
- //console.log("Getting user info", keyToUse, cacheData);
82
- return cacheData;
83
- } else {
84
- //console.log("Getting user info", keyToUse, data);
85
- }
80
+ console.log("Getting user info", { cacheData, keyToUse, data });
81
+ if (cacheData) return cacheData;
86
82
 
87
83
  if (keyToUse?.includes(".eth")) {
88
84
  const response = await fetch(`https://ensdata.net/${keyToUse}`);
@@ -124,3 +120,17 @@ export const isOnXMTP = async (
124
120
  if (domain == "fabri.eth") return false;
125
121
  if (address) return (await client.canMessage([address])).length > 0;
126
122
  };
123
+
124
+ export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
125
+ let { address, ensDomain, converseUsername, preferredName } = userInfo;
126
+ let prompt = `User context:
127
+ - Start by fetch their domain from or Convese username
128
+ - Call the user by their name or domain, in case they have one
129
+ - Ask for a name (if they don't have one) so you can suggest domains.
130
+ - Users address is: ${address}`;
131
+ if (preferredName) prompt += `\n- Users name is: ${preferredName}`;
132
+ if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
133
+ if (converseUsername)
134
+ prompt += `\n- Converse username is: ${converseUsername}`;
135
+ return prompt;
136
+ };
@@ -0,0 +1,43 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+
4
+ import OpenAI from "openai";
5
+ const openai = new OpenAI({
6
+ apiKey: process.env.OPEN_AI_API_KEY,
7
+ });
8
+
9
+ export async function vision(imageData: Uint8Array, systemPrompt: string) {
10
+ const base64Image = Buffer.from(imageData).toString("base64");
11
+ const dataUrl = `data:image/jpeg;base64,${base64Image}`;
12
+
13
+ // Create a new thread for each vision request
14
+ const visionMessages = [
15
+ {
16
+ role: "system",
17
+ content: systemPrompt,
18
+ },
19
+ {
20
+ role: "user",
21
+ content: [
22
+ { type: "text", text: systemPrompt },
23
+ {
24
+ type: "image_url",
25
+ image_url: {
26
+ url: dataUrl,
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ ];
32
+
33
+ try {
34
+ const response = await openai.chat.completions.create({
35
+ model: "gpt-4o",
36
+ messages: visionMessages as any,
37
+ });
38
+ return response.choices[0].message.content;
39
+ } catch (error) {
40
+ console.error("Failed to interpret image with OpenAI:", error);
41
+ throw error;
42
+ }
43
+ }
@@ -14,6 +14,7 @@ export const skills: SkillGroup[] = [
14
14
  {
15
15
  command: "/tip [@usernames] [amount] [token]",
16
16
  triggers: ["/tip"],
17
+ examples: ["/tip @vitalik 10 usdc"],
17
18
  description: "Tip users in a specified token.",
18
19
  handler: tipping,
19
20
  params: {
@@ -37,6 +38,7 @@ export const skills: SkillGroup[] = [
37
38
  {
38
39
  command: "/send [amount] [token] [username]",
39
40
  triggers: ["/send"],
41
+ examples: ["/send 10 usdc @vitalik"],
40
42
  description:
41
43
  "Send a specified amount of a cryptocurrency to a destination address.",
42
44
  handler: transaction,
@@ -59,6 +61,7 @@ export const skills: SkillGroup[] = [
59
61
  {
60
62
  command: "/swap [amount] [token_from] [token_to]",
61
63
  triggers: ["/swap"],
64
+ examples: ["/swap 10 usdc eth"],
62
65
  description: "Exchange one type of cryptocurrency for another.",
63
66
  handler: transaction,
64
67
  params: {
@@ -81,6 +84,7 @@ export const skills: SkillGroup[] = [
81
84
  {
82
85
  command: "/show",
83
86
  triggers: ["/show"],
87
+ examples: ["/show"],
84
88
  handler: transaction,
85
89
  description: "Show the whole frame.",
86
90
  params: {},
@@ -113,6 +117,7 @@ export const skills: SkillGroup[] = [
113
117
  {
114
118
  command: "/points",
115
119
  triggers: ["/points"],
120
+ examples: ["/points"],
116
121
  handler: loyalty,
117
122
  description: "Check your points.",
118
123
  params: {},
@@ -134,6 +139,7 @@ export const skills: SkillGroup[] = [
134
139
  {
135
140
  command: "/agent [prompt]",
136
141
  triggers: ["/agent", "@agent", "@bot"],
142
+ examples: ["/agent @vitalik"],
137
143
  handler: agent,
138
144
  description: "Manage agent commands.",
139
145
  params: {
@@ -152,6 +158,7 @@ export const skills: SkillGroup[] = [
152
158
  {
153
159
  command: "/help",
154
160
  triggers: ["/help"],
161
+ examples: ["/help"],
155
162
  handler: helpHandler,
156
163
  description: "Get help with the bot.",
157
164
  params: {},