create-message-kit 1.1.5 → 1.1.7-beta.2

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -15,3 +15,7 @@ npx create-message-kit@latest
15
15
  ```bash
16
16
  yarn create message-kit
17
17
  ```
18
+
19
+ ```bash [npm]
20
+ npm init message-kit@latest
21
+ ```
package/index.js CHANGED
@@ -14,15 +14,11 @@ const packageJson = JSON.parse(
14
14
  fs.readFileSync(resolve(__dirname, "package.json"), "utf8"),
15
15
  );
16
16
  const version = packageJson.version;
17
- const pckMessageKitLib = JSON.parse(
18
- fs.readFileSync(resolve(__dirname, "../message-kit/package.json"), "utf8"),
19
- );
20
- const versionMessageKitLib = pckMessageKitLib.version;
21
17
  program
22
18
  .name("byob")
23
19
  .description("CLI to initialize projects")
24
20
  .action(async () => {
25
- log.info(pc.cyan(`Welcome to MessageKit v${versionMessageKitLib}!`));
21
+ log.info(pc.cyan(`Welcome to MessageKit CLI v${version}!`));
26
22
  const coolLogo = `
27
23
  ███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
28
24
  ████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
@@ -183,7 +179,6 @@ function createGitignore(destDir) {
183
179
  # Main
184
180
  .env
185
181
  node_modules/
186
- .gitignore
187
182
  .data/
188
183
  dist/
189
184
  .DS_Store
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-message-kit",
3
- "version": "1.1.5",
3
+ "version": "1.1.7-beta.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,17 +1,20 @@
1
- import { HandlerContext } from "@xmtp/message-kit";
1
+ import { HandlerContext, SkillResponse } from "@xmtp/message-kit";
2
2
  import { getUserInfo, clearInfoCache, isOnXMTP } from "../lib/resolver.js";
3
3
  import { isAddress } from "viem";
4
- import { clearMemory } from "../lib/openai.js";
4
+ import { clearMemory } from "../lib/gpt.js";
5
5
 
6
6
  export const frameUrl = "https://ens.steer.fun/";
7
7
  export const ensUrl = "https://app.ens.domains/";
8
8
  export const baseTxUrl = "https://base-tx-frame.vercel.app";
9
9
 
10
- export async function handleEns(context: HandlerContext) {
10
+ export async function handleEns(
11
+ context: HandlerContext,
12
+ ): Promise<SkillResponse> {
11
13
  const {
12
14
  message: {
13
15
  content: { command, params, sender },
14
16
  },
17
+ skill,
15
18
  } = context;
16
19
  if (command == "reset") {
17
20
  clearMemory();
@@ -115,7 +118,7 @@ export async function handleEns(context: HandlerContext) {
115
118
  };
116
119
  } else {
117
120
  let message = `Looks like ${domain} is already registered!`;
118
- await context.skill("/cool " + domain);
121
+ await skill("/cool " + domain);
119
122
  return {
120
123
  code: 404,
121
124
  message,
@@ -145,6 +148,8 @@ export async function handleEns(context: HandlerContext) {
145
148
  code: 200,
146
149
  message: `${generateCoolAlternatives(domain)}`,
147
150
  };
151
+ } else {
152
+ return { code: 400, message: "Command not found." };
148
153
  }
149
154
  }
150
155
 
@@ -1,10 +1,10 @@
1
1
  import { run, HandlerContext } from "@xmtp/message-kit";
2
- import { textGeneration, processResponseWithSkill } from "./lib/openai.js";
2
+ import { textGeneration, processMultilineResponse } from "./lib/gpt.js";
3
3
  import { agent_prompt } from "./prompt.js";
4
4
  import { getUserInfo } from "./lib/resolver.js";
5
5
 
6
6
  run(async (context: HandlerContext) => {
7
- /*All the commands are handled through the commands file*/
7
+ /*All the skills are handled through the skills file*/
8
8
  /* If its just text, it will be handled by the ensAgent*/
9
9
  /* If its a group message, it will be handled by the groupAgent*/
10
10
  if (!process?.env?.OPEN_AI_API_KEY) {
@@ -31,7 +31,7 @@ run(async (context: HandlerContext) => {
31
31
  userPrompt,
32
32
  await agent_prompt(userInfo),
33
33
  );
34
- await processResponseWithSkill(sender.address, reply, context);
34
+ await processMultilineResponse(sender.address, reply, context);
35
35
  } catch (error) {
36
36
  console.error("Error during OpenAI call:", error);
37
37
  await context.send("An error occurred while processing your request.");
@@ -0,0 +1,161 @@
1
+ import "dotenv/config";
2
+ import type { SkillGroup } from "@xmtp/message-kit";
3
+ import OpenAI from "openai";
4
+ const openai = new OpenAI({
5
+ apiKey: process.env.OPEN_AI_API_KEY,
6
+ });
7
+
8
+ type ChatHistoryEntry = { role: string; content: string };
9
+ type ChatHistories = Record<string, ChatHistoryEntry[]>;
10
+ // New ChatMemory class
11
+ class ChatMemory {
12
+ private histories: ChatHistories = {};
13
+
14
+ getHistory(address: string): ChatHistoryEntry[] {
15
+ return this.histories[address] || [];
16
+ }
17
+
18
+ addEntry(address: string, entry: ChatHistoryEntry) {
19
+ if (!this.histories[address]) {
20
+ this.histories[address] = [];
21
+ }
22
+ this.histories[address].push(entry);
23
+ }
24
+
25
+ initializeWithSystem(address: string, systemPrompt: string) {
26
+ if (this.getHistory(address).length === 0) {
27
+ this.addEntry(address, {
28
+ role: "system",
29
+ content: systemPrompt,
30
+ });
31
+ }
32
+ }
33
+
34
+ clear() {
35
+ this.histories = {};
36
+ }
37
+ }
38
+
39
+ // Create singleton instance
40
+ export const chatMemory = new ChatMemory();
41
+
42
+ export const clearMemory = () => {
43
+ chatMemory.clear();
44
+ };
45
+
46
+ export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
47
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
48
+ - You can trigger skills by only sending the command in a newline message.
49
+ - Never announce actions without using a command separated by a newline character.
50
+ - Dont answer in markdown format, just answer in plaintext.
51
+ - Do not make guesses or assumptions
52
+ - Only answer if the verified information is in the prompt.
53
+ - Check that you are not missing a command
54
+ - Focus only on helping users with operations detailed below.
55
+ `;
56
+
57
+ export function PROMPT_SKILLS_AND_EXAMPLES(skills: SkillGroup[], tag: string) {
58
+ let foundSkills = skills.filter(
59
+ (skill) => skill.tag == `@${tag.toLowerCase()}`,
60
+ );
61
+ if (!foundSkills.length || !foundSkills[0] || !foundSkills[0].skills)
62
+ return "";
63
+ let returnPrompt = `\nCommands:\n${foundSkills[0].skills
64
+ .map((skill) => skill.command)
65
+ .join("\n")}\n\nExamples:\n${foundSkills[0].skills
66
+ .map((skill) => skill.examples)
67
+ .join("\n")}`;
68
+ return returnPrompt;
69
+ }
70
+
71
+ export async function textGeneration(
72
+ memoryKey: string,
73
+ userPrompt: string,
74
+ systemPrompt: string,
75
+ ) {
76
+ if (!memoryKey) {
77
+ clearMemory();
78
+ }
79
+ let messages = chatMemory.getHistory(memoryKey);
80
+ chatMemory.initializeWithSystem(memoryKey, systemPrompt);
81
+ if (messages.length === 0) {
82
+ messages.push({
83
+ role: "system",
84
+ content: systemPrompt,
85
+ });
86
+ }
87
+ messages.push({
88
+ role: "user",
89
+ content: userPrompt,
90
+ });
91
+ try {
92
+ const response = await openai.chat.completions.create({
93
+ model: "gpt-4o",
94
+ messages: messages as any,
95
+ });
96
+ const reply = response.choices[0].message.content;
97
+ messages.push({
98
+ role: "assistant",
99
+ content: reply || "No response from OpenAI.",
100
+ });
101
+ const cleanedReply = parseMarkdown(reply as string);
102
+ chatMemory.addEntry(memoryKey, {
103
+ role: "assistant",
104
+ content: cleanedReply,
105
+ });
106
+ return { reply: cleanedReply, history: messages };
107
+ } catch (error) {
108
+ console.error("Failed to fetch from OpenAI:", error);
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ export async function processMultilineResponse(
114
+ memoryKey: string,
115
+ reply: string,
116
+ context: any,
117
+ ) {
118
+ if (!memoryKey) {
119
+ clearMemory();
120
+ }
121
+ let messages = reply
122
+ .split("\n")
123
+ .map((message: string) => parseMarkdown(message))
124
+ .filter((message): message is string => message.length > 0);
125
+
126
+ console.log(messages);
127
+ for (const message of messages) {
128
+ if (message.startsWith("/")) {
129
+ const response = await context.skill(message);
130
+ if (response && typeof response.message === "string") {
131
+ let msg = parseMarkdown(response.message);
132
+ chatMemory.addEntry(memoryKey, {
133
+ role: "system",
134
+ content: msg,
135
+ });
136
+ await context.send(response.message);
137
+ }
138
+ } else {
139
+ await context.send(message);
140
+ }
141
+ }
142
+ }
143
+ export function parseMarkdown(message: string) {
144
+ let trimmedMessage = message;
145
+ // Remove bold and underline markdown
146
+ trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
147
+ // Remove markdown links, keeping only the URL
148
+ trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
149
+ // Remove markdown headers
150
+ trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
151
+ // Remove inline code formatting
152
+ trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
153
+ // Remove single backticks at the start or end of the message
154
+ trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
155
+ // Remove leading and trailing whitespace
156
+ trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
157
+ // Remove any remaining leading or trailing whitespace
158
+ trimmedMessage = trimmedMessage.trim();
159
+
160
+ return trimmedMessage;
161
+ }
@@ -48,7 +48,7 @@ export const chatMemory = new ChatMemory();
48
48
  let chatHistories: ChatHistories = {};
49
49
  export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
50
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.
51
+ - You can trigger skills by only sending the command in a newline message.
52
52
  - Never announce actions without using a command separated by a newline character.
53
53
  - Dont answer in markdown format, just answer in plaintext.
54
54
  - Do not make guesses or assumptions
@@ -81,7 +81,7 @@ export async function agentResponse(
81
81
  userPrompt,
82
82
  systemPrompt,
83
83
  );
84
- await processResponseWithSkill(sender.address, reply, context);
84
+ await processMultilineResponse(sender.address, reply, context);
85
85
  } catch (error) {
86
86
  console.error("Error during OpenAI call:", error);
87
87
  await context.reply("An error occurred while processing your request.");
@@ -126,7 +126,7 @@ export async function textGeneration(
126
126
  }
127
127
  }
128
128
 
129
- export async function processResponseWithSkill(
129
+ export async function processMultilineResponse(
130
130
  address: string,
131
131
  reply: string,
132
132
  context: any,
@@ -1,5 +1,6 @@
1
- import { Client } from "@xmtp/xmtp-js";
1
+ import type { Client } from "@xmtp/xmtp-js";
2
2
  import { isAddress } from "viem";
3
+ import type { HandlerContext } from "@xmtp/message-kit";
3
4
 
4
5
  export const converseEndpointURL =
5
6
  "https://converse-website-git-endpoit-ephemerahq.vercel.app";
@@ -16,8 +17,8 @@ export type ConverseProfile = {
16
17
  export type UserInfo = {
17
18
  ensDomain?: string | undefined;
18
19
  address?: string | undefined;
20
+ preferredName: string | undefined;
19
21
  converseUsername?: string | undefined;
20
- preferredName?: string | undefined;
21
22
  ensInfo?: EnsData | undefined;
22
23
  avatar?: string | undefined;
23
24
  };
@@ -48,12 +49,14 @@ export const clearInfoCache = () => {
48
49
  export const getUserInfo = async (
49
50
  key: string,
50
51
  clientAddress?: string,
52
+ context?: HandlerContext,
51
53
  ): Promise<UserInfo | null> => {
52
54
  let data: UserInfo = infoCache.get(key) || {
53
55
  ensDomain: undefined,
54
56
  address: undefined,
55
57
  converseUsername: undefined,
56
58
  ensInfo: undefined,
59
+ preferredName: undefined,
57
60
  };
58
61
  if (isAddress(clientAddress || "")) {
59
62
  data.address = clientAddress;
@@ -74,12 +77,15 @@ export const getUserInfo = async (
74
77
  } else {
75
78
  data.converseUsername = key;
76
79
  }
77
-
80
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
78
81
  let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
82
  let cacheData = keyToUse && infoCache.get(keyToUse);
80
- console.log("Getting user info", { cacheData, keyToUse, data });
83
+ //console.log("Getting user info", { cacheData, keyToUse, data });
81
84
  if (cacheData) return cacheData;
82
85
 
86
+ context?.send(
87
+ "Hey there! Give me a sec while I fetch info about you first...",
88
+ );
83
89
  if (keyToUse?.includes(".eth")) {
84
90
  const response = await fetch(`https://ensdata.net/${keyToUse}`);
85
91
  const ensData: EnsData = (await response.json()) as EnsData;
@@ -102,13 +108,15 @@ export const getUserInfo = async (
102
108
  }),
103
109
  });
104
110
  const converseData = (await response.json()) as ConverseProfile;
105
- if (process.env.MSG_LOG)
111
+ if (process.env.MSG_LOG === "true")
106
112
  console.log("Converse data", keyToUse, converseData);
107
113
  data.converseUsername =
108
114
  converseData?.formattedName || converseData?.name || undefined;
109
115
  data.address = converseData?.address || undefined;
110
116
  data.avatar = converseData?.avatar || undefined;
111
117
  }
118
+
119
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
112
120
  if (data.address) infoCache.set(data.address, data);
113
121
  return data;
114
122
  };
@@ -123,7 +131,8 @@ export const isOnXMTP = async (
123
131
 
124
132
  export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
125
133
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
126
- let prompt = `User context:
134
+ let prompt = `
135
+ User context:
127
136
  - Start by fetch their domain from or Convese username
128
137
  - Call the user by their name or domain, in case they have one
129
138
  - Ask for a name (if they don't have one) so you can suggest domains.
@@ -132,5 +141,11 @@ export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
132
141
  if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
133
142
  if (converseUsername)
134
143
  prompt += `\n- Converse username is: ${converseUsername}`;
144
+
145
+ prompt = prompt.replace("{ADDRESS}", address || "");
146
+ prompt = prompt.replace("{ENS_DOMAIN}", ensDomain || "");
147
+ prompt = prompt.replace("{CONVERSE_USERNAME}", converseUsername || "");
148
+ prompt = prompt.replace("{PREFERRED_NAME}", preferredName || "");
149
+
135
150
  return prompt;
136
151
  };
@@ -1,6 +1,6 @@
1
1
  import { skills } from "./skills.js";
2
2
  import { UserInfo, PROMPT_USER_CONTENT } from "./lib/resolver.js";
3
- import { PROMPT_RULES, PROMPT_SKILLS_AND_EXAMPLES } from "./lib/openai.js";
3
+ import { PROMPT_RULES, PROMPT_SKILLS_AND_EXAMPLES } from "./lib/gpt.js";
4
4
 
5
5
  export async function agent_prompt(userInfo: UserInfo) {
6
6
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
@@ -12,7 +12,7 @@ export async function agent_prompt(userInfo: UserInfo) {
12
12
  systemPrompt += PROMPT_USER_CONTENT(userInfo);
13
13
 
14
14
  //Add skills and examples to the prompt
15
- systemPrompt += PROMPT_SKILLS_AND_EXAMPLES(skills);
15
+ systemPrompt += PROMPT_SKILLS_AND_EXAMPLES(skills, "@ens");
16
16
 
17
17
  systemPrompt += `
18
18
 
@@ -20,6 +20,19 @@ export const skills: SkillGroup[] = [
20
20
  },
21
21
  },
22
22
  },
23
+ {
24
+ command: "/exists",
25
+ adminOnly: true,
26
+ examples: ["/exists"],
27
+ handler: handleEns,
28
+ triggers: ["/exists"],
29
+ description: "Check if an address is onboarded.",
30
+ params: {
31
+ address: {
32
+ type: "address",
33
+ },
34
+ },
35
+ },
23
36
  {
24
37
  command: "/info [domain]",
25
38
  triggers: ["/info"],
@@ -1,5 +1,5 @@
1
1
  import { HandlerContext, AbstractedMember } from "@xmtp/message-kit";
2
- import { textGeneration } from "../lib/openai.js";
2
+ import { textGeneration } from "../lib/gpt.js";
3
3
 
4
4
  export async function handler(context: HandlerContext) {
5
5
  if (!process?.env?.OPEN_AI_API_KEY) {
@@ -12,6 +12,7 @@ export async function handler(context: HandlerContext) {
12
12
  sender,
13
13
  content: { content, params },
14
14
  },
15
+ skill,
15
16
  } = context;
16
17
 
17
18
  const systemPrompt = generateSystemPrompt(context);
@@ -23,7 +24,7 @@ export async function handler(context: HandlerContext) {
23
24
  userPrompt,
24
25
  systemPrompt,
25
26
  );
26
- context.skill(reply);
27
+ skill(reply);
27
28
  } catch (error) {
28
29
  console.error("Error during OpenAI call:", error);
29
30
  await context.reply("An error occurred while processing your request.");
@@ -59,7 +60,7 @@ function generateSystemPrompt(context: HandlerContext) {
59
60
  Important:
60
61
  - If a user asks jokes, make jokes about web3 devs\n
61
62
  - If the user asks about performing an action and you can think of a command that would help, answer directly with the command and nothing else.
62
- - Populate the command with the correct or random values. Always return commands with real values only, using usernames with @ and excluding addresses.\n
63
+ - Populate the command with the correct or random values. Always return skills with real values only, using usernames with @ and excluding addresses.\n
63
64
  - If the user asks a question or makes a statement that does not clearly map to a command, respond with helpful information or a clarification question.\n
64
65
  - If the user is grateful, respond asking for a tip in a playful manner.
65
66
  `;
@@ -34,7 +34,7 @@ export async function handler(context: HandlerContext) {
34
34
  context.send("Available games: \n/game wordle\n/game slot");
35
35
  break;
36
36
  default:
37
- // Inform the user about unrecognized commands and provide available options
37
+ // Inform the user about unrecognized skills and provide available options
38
38
  context.send(
39
39
  "Command not recognized. Available games: wordle, slot, or help.",
40
40
  );
@@ -0,0 +1,24 @@
1
+ import { HandlerContext } from "@xmtp/message-kit";
2
+
3
+ export async function handler(context: HandlerContext) {
4
+ const {
5
+ skills,
6
+ group,
7
+ message: {
8
+ content: { command },
9
+ },
10
+ } = context;
11
+
12
+ if (command == "help") {
13
+ const intro =
14
+ "Available experiences:\n" +
15
+ skills
16
+ ?.flatMap((app) => app.skills)
17
+ .map((skill) => `${skill.command} - ${skill.description}`)
18
+ .join("\n") +
19
+ "\nUse these skills to interact with specific apps.";
20
+ context.send(intro);
21
+ } else if (command == "id") {
22
+ context.send(context.group?.id);
23
+ }
24
+ }
@@ -1,5 +1,5 @@
1
1
  import { HandlerContext } from "@xmtp/message-kit";
2
- import { textGeneration } from "../lib/openai.js";
2
+ import { textGeneration } from "../lib/gpt.js";
3
3
  import { vision } from "../lib/vision.js";
4
4
  import { getUserInfo } from "../lib/resolver.js";
5
5
 
@@ -10,7 +10,7 @@ export async function handler(context: HandlerContext) {
10
10
  }
11
11
  const {
12
12
  members,
13
- skills,
13
+ skill,
14
14
  message: {
15
15
  typeId,
16
16
  content: { attachment },
@@ -57,7 +57,7 @@ export async function handler(context: HandlerContext) {
57
57
  let splitMessages = JSON.parse(reply);
58
58
  for (const message of splitMessages) {
59
59
  let msg = message as string;
60
- if (msg.startsWith("/")) await context.skill(msg);
60
+ if (msg.startsWith("/")) await skill(msg);
61
61
  else await context.send(msg);
62
62
  }
63
63
  }
@@ -7,7 +7,6 @@ export async function handler(context: HandlerContext) {
7
7
  getMessageById,
8
8
  message: { content, sender, typeId },
9
9
  } = context;
10
- console.log(sender);
11
10
  const msg = await getMessageById(content.reference);
12
11
  const replyReceiver = members?.find(
13
12
  (member) => member.inboxId === msg?.senderInboxId,
@@ -28,7 +27,7 @@ export async function handler(context: HandlerContext) {
28
27
  } else if (typeId === "text") {
29
28
  const { content: text, params } = content;
30
29
  if (text.startsWith("/tip") && params) {
31
- // Process text commands starting with "/tip"
30
+ // Process text skills starting with "/tip"
32
31
  const {
33
32
  params: { amount: extractedAmount, username },
34
33
  } = content;
@@ -43,7 +42,6 @@ export async function handler(context: HandlerContext) {
43
42
  context.reply("Sender or receiver or amount not found.");
44
43
  return;
45
44
  }
46
- console.log(receivers);
47
45
  const receiverAddresses = receivers.map((receiver) => receiver.address);
48
46
  // Process sending tokens to each receiver
49
47
 
@@ -21,7 +21,6 @@ export async function handler(context: HandlerContext) {
21
21
  );
22
22
  return;
23
23
  }
24
- let name = senderInfo.converseUsername || senderInfo.address;
25
24
 
26
25
  let sendUrl = `${baseUrl}/?transaction_type=send&amount=${amountSend}&token=${tokenSend}&receiver=${senderInfo.address}`;
27
26
  context.send(`${sendUrl}`);
@@ -1,21 +1,18 @@
1
1
  import { run, HandlerContext } from "@xmtp/message-kit";
2
- import { handler as splitpayment } from "./handler/splitpayment.js";
3
2
 
4
3
  // Main function to run the app
5
4
  run(
6
5
  async (context: HandlerContext) => {
7
6
  const {
8
7
  message: { typeId },
8
+ group,
9
9
  } = context;
10
10
  switch (typeId) {
11
11
  case "reply":
12
12
  handleReply(context);
13
13
  break;
14
- case "remoteStaticAttachment":
15
- handleAttachment(context);
16
- break;
17
14
  }
18
- if (!context.group) {
15
+ if (!group) {
19
16
  context.send("This is a group bot, add this address to a group");
20
17
  }
21
18
  },
@@ -38,20 +35,3 @@ async function handleReply(context: HandlerContext) {
38
35
  );
39
36
  //await context.skill(chain);
40
37
  }
41
-
42
- // Handle attachment messages
43
- async function handleAttachment(context: HandlerContext) {
44
- await splitpayment(context);
45
- }
46
-
47
- export async function helpHandler(context: HandlerContext) {
48
- const { skills } = context;
49
- const intro =
50
- "Available experiences:\n" +
51
- skills
52
- ?.flatMap((app) => app.skills)
53
- .map((skill) => `${skill.command} - ${skill.description}`)
54
- .join("\n") +
55
- "\nUse these commands to interact with specific apps.";
56
- context.send(intro);
57
- }
@@ -0,0 +1,161 @@
1
+ import "dotenv/config";
2
+ import type { SkillGroup } from "@xmtp/message-kit";
3
+ import OpenAI from "openai";
4
+ const openai = new OpenAI({
5
+ apiKey: process.env.OPEN_AI_API_KEY,
6
+ });
7
+
8
+ type ChatHistoryEntry = { role: string; content: string };
9
+ type ChatHistories = Record<string, ChatHistoryEntry[]>;
10
+ // New ChatMemory class
11
+ class ChatMemory {
12
+ private histories: ChatHistories = {};
13
+
14
+ getHistory(address: string): ChatHistoryEntry[] {
15
+ return this.histories[address] || [];
16
+ }
17
+
18
+ addEntry(address: string, entry: ChatHistoryEntry) {
19
+ if (!this.histories[address]) {
20
+ this.histories[address] = [];
21
+ }
22
+ this.histories[address].push(entry);
23
+ }
24
+
25
+ initializeWithSystem(address: string, systemPrompt: string) {
26
+ if (this.getHistory(address).length === 0) {
27
+ this.addEntry(address, {
28
+ role: "system",
29
+ content: systemPrompt,
30
+ });
31
+ }
32
+ }
33
+
34
+ clear() {
35
+ this.histories = {};
36
+ }
37
+ }
38
+
39
+ // Create singleton instance
40
+ export const chatMemory = new ChatMemory();
41
+
42
+ export const clearMemory = () => {
43
+ chatMemory.clear();
44
+ };
45
+
46
+ export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
47
+ - You can respond with multiple messages if needed. Each message should be separated by a newline character.
48
+ - You can trigger skills by only sending the command in a newline message.
49
+ - Never announce actions without using a command separated by a newline character.
50
+ - Dont answer in markdown format, just answer in plaintext.
51
+ - Do not make guesses or assumptions
52
+ - Only answer if the verified information is in the prompt.
53
+ - Check that you are not missing a command
54
+ - Focus only on helping users with operations detailed below.
55
+ `;
56
+
57
+ export function PROMPT_SKILLS_AND_EXAMPLES(skills: SkillGroup[], tag: string) {
58
+ let foundSkills = skills.filter(
59
+ (skill) => skill.tag == `@${tag.toLowerCase()}`,
60
+ );
61
+ if (!foundSkills.length || !foundSkills[0] || !foundSkills[0].skills)
62
+ return "";
63
+ let returnPrompt = `\nCommands:\n${foundSkills[0].skills
64
+ .map((skill) => skill.command)
65
+ .join("\n")}\n\nExamples:\n${foundSkills[0].skills
66
+ .map((skill) => skill.examples)
67
+ .join("\n")}`;
68
+ return returnPrompt;
69
+ }
70
+
71
+ export async function textGeneration(
72
+ memoryKey: string,
73
+ userPrompt: string,
74
+ systemPrompt: string,
75
+ ) {
76
+ if (!memoryKey) {
77
+ clearMemory();
78
+ }
79
+ let messages = chatMemory.getHistory(memoryKey);
80
+ chatMemory.initializeWithSystem(memoryKey, systemPrompt);
81
+ if (messages.length === 0) {
82
+ messages.push({
83
+ role: "system",
84
+ content: systemPrompt,
85
+ });
86
+ }
87
+ messages.push({
88
+ role: "user",
89
+ content: userPrompt,
90
+ });
91
+ try {
92
+ const response = await openai.chat.completions.create({
93
+ model: "gpt-4o",
94
+ messages: messages as any,
95
+ });
96
+ const reply = response.choices[0].message.content;
97
+ messages.push({
98
+ role: "assistant",
99
+ content: reply || "No response from OpenAI.",
100
+ });
101
+ const cleanedReply = parseMarkdown(reply as string);
102
+ chatMemory.addEntry(memoryKey, {
103
+ role: "assistant",
104
+ content: cleanedReply,
105
+ });
106
+ return { reply: cleanedReply, history: messages };
107
+ } catch (error) {
108
+ console.error("Failed to fetch from OpenAI:", error);
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ export async function processMultilineResponse(
114
+ memoryKey: string,
115
+ reply: string,
116
+ context: any,
117
+ ) {
118
+ if (!memoryKey) {
119
+ clearMemory();
120
+ }
121
+ let messages = reply
122
+ .split("\n")
123
+ .map((message: string) => parseMarkdown(message))
124
+ .filter((message): message is string => message.length > 0);
125
+
126
+ console.log(messages);
127
+ for (const message of messages) {
128
+ if (message.startsWith("/")) {
129
+ const response = await context.skill(message);
130
+ if (response && typeof response.message === "string") {
131
+ let msg = parseMarkdown(response.message);
132
+ chatMemory.addEntry(memoryKey, {
133
+ role: "system",
134
+ content: msg,
135
+ });
136
+ await context.send(response.message);
137
+ }
138
+ } else {
139
+ await context.send(message);
140
+ }
141
+ }
142
+ }
143
+ export function parseMarkdown(message: string) {
144
+ let trimmedMessage = message;
145
+ // Remove bold and underline markdown
146
+ trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
147
+ // Remove markdown links, keeping only the URL
148
+ trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
149
+ // Remove markdown headers
150
+ trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
151
+ // Remove inline code formatting
152
+ trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
153
+ // Remove single backticks at the start or end of the message
154
+ trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
155
+ // Remove leading and trailing whitespace
156
+ trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
157
+ // Remove any remaining leading or trailing whitespace
158
+ trimmedMessage = trimmedMessage.trim();
159
+
160
+ return trimmedMessage;
161
+ }
@@ -48,7 +48,7 @@ export const chatMemory = new ChatMemory();
48
48
  let chatHistories: ChatHistories = {};
49
49
  export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
50
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.
51
+ - You can trigger skills by only sending the command in a newline message.
52
52
  - Never announce actions without using a command separated by a newline character.
53
53
  - Dont answer in markdown format, just answer in plaintext.
54
54
  - Do not make guesses or assumptions
@@ -81,7 +81,7 @@ export async function agentResponse(
81
81
  userPrompt,
82
82
  systemPrompt,
83
83
  );
84
- await processResponseWithSkill(sender.address, reply, context);
84
+ await processMultilineResponse(sender.address, reply, context);
85
85
  } catch (error) {
86
86
  console.error("Error during OpenAI call:", error);
87
87
  await context.reply("An error occurred while processing your request.");
@@ -126,7 +126,7 @@ export async function textGeneration(
126
126
  }
127
127
  }
128
128
 
129
- export async function processResponseWithSkill(
129
+ export async function processMultilineResponse(
130
130
  address: string,
131
131
  reply: string,
132
132
  context: any,
@@ -1,5 +1,6 @@
1
- import { Client } from "@xmtp/xmtp-js";
1
+ import type { Client } from "@xmtp/xmtp-js";
2
2
  import { isAddress } from "viem";
3
+ import type { HandlerContext } from "@xmtp/message-kit";
3
4
 
4
5
  export const converseEndpointURL =
5
6
  "https://converse-website-git-endpoit-ephemerahq.vercel.app";
@@ -16,8 +17,8 @@ export type ConverseProfile = {
16
17
  export type UserInfo = {
17
18
  ensDomain?: string | undefined;
18
19
  address?: string | undefined;
20
+ preferredName: string | undefined;
19
21
  converseUsername?: string | undefined;
20
- preferredName?: string | undefined;
21
22
  ensInfo?: EnsData | undefined;
22
23
  avatar?: string | undefined;
23
24
  };
@@ -48,12 +49,14 @@ export const clearInfoCache = () => {
48
49
  export const getUserInfo = async (
49
50
  key: string,
50
51
  clientAddress?: string,
52
+ context?: HandlerContext,
51
53
  ): Promise<UserInfo | null> => {
52
54
  let data: UserInfo = infoCache.get(key) || {
53
55
  ensDomain: undefined,
54
56
  address: undefined,
55
57
  converseUsername: undefined,
56
58
  ensInfo: undefined,
59
+ preferredName: undefined,
57
60
  };
58
61
  if (isAddress(clientAddress || "")) {
59
62
  data.address = clientAddress;
@@ -74,12 +77,15 @@ export const getUserInfo = async (
74
77
  } else {
75
78
  data.converseUsername = key;
76
79
  }
77
-
80
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
78
81
  let keyToUse = data.address || data.ensDomain || data.converseUsername;
79
82
  let cacheData = keyToUse && infoCache.get(keyToUse);
80
- console.log("Getting user info", { cacheData, keyToUse, data });
83
+ //console.log("Getting user info", { cacheData, keyToUse, data });
81
84
  if (cacheData) return cacheData;
82
85
 
86
+ context?.send(
87
+ "Hey there! Give me a sec while I fetch info about you first...",
88
+ );
83
89
  if (keyToUse?.includes(".eth")) {
84
90
  const response = await fetch(`https://ensdata.net/${keyToUse}`);
85
91
  const ensData: EnsData = (await response.json()) as EnsData;
@@ -102,13 +108,15 @@ export const getUserInfo = async (
102
108
  }),
103
109
  });
104
110
  const converseData = (await response.json()) as ConverseProfile;
105
- if (process.env.MSG_LOG)
111
+ if (process.env.MSG_LOG === "true")
106
112
  console.log("Converse data", keyToUse, converseData);
107
113
  data.converseUsername =
108
114
  converseData?.formattedName || converseData?.name || undefined;
109
115
  data.address = converseData?.address || undefined;
110
116
  data.avatar = converseData?.avatar || undefined;
111
117
  }
118
+
119
+ data.preferredName = data.ensDomain || data.converseUsername || "Friend";
112
120
  if (data.address) infoCache.set(data.address, data);
113
121
  return data;
114
122
  };
@@ -123,7 +131,8 @@ export const isOnXMTP = async (
123
131
 
124
132
  export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
125
133
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
126
- let prompt = `User context:
134
+ let prompt = `
135
+ User context:
127
136
  - Start by fetch their domain from or Convese username
128
137
  - Call the user by their name or domain, in case they have one
129
138
  - Ask for a name (if they don't have one) so you can suggest domains.
@@ -132,5 +141,11 @@ export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
132
141
  if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
133
142
  if (converseUsername)
134
143
  prompt += `\n- Converse username is: ${converseUsername}`;
144
+
145
+ prompt = prompt.replace("{ADDRESS}", address || "");
146
+ prompt = prompt.replace("{ENS_DOMAIN}", ensDomain || "");
147
+ prompt = prompt.replace("{CONVERSE_USERNAME}", converseUsername || "");
148
+ prompt = prompt.replace("{PREFERRED_NAME}", preferredName || "");
149
+
135
150
  return prompt;
136
151
  };
@@ -1,5 +1,4 @@
1
- import dotenv from "dotenv";
2
- dotenv.config();
1
+ import "dotenv/config";
3
2
 
4
3
  import OpenAI from "openai";
5
4
  const openai = new OpenAI({
@@ -3,7 +3,7 @@ import { handler as agent } from "./handler/agent.js";
3
3
  import { handler as transaction } from "./handler/transaction.js";
4
4
  import { handler as games } from "./handler/game.js";
5
5
  import { handler as loyalty } from "./handler/loyalty.js";
6
- import { helpHandler } from "./index.js";
6
+ import { handler as groupHelp } from "./handler/group.js";
7
7
  import type { SkillGroup } from "@xmtp/message-kit";
8
8
 
9
9
  export const skills: SkillGroup[] = [
@@ -31,6 +31,7 @@ export const skills: SkillGroup[] = [
31
31
  },
32
32
  ],
33
33
  },
34
+
34
35
  {
35
36
  name: "Transactions",
36
37
  description: "Multipurpose transaction frame built onbase.",
@@ -159,10 +160,18 @@ export const skills: SkillGroup[] = [
159
160
  command: "/help",
160
161
  triggers: ["/help"],
161
162
  examples: ["/help"],
162
- handler: helpHandler,
163
+ handler: groupHelp,
163
164
  description: "Get help with the bot.",
164
165
  params: {},
165
166
  },
167
+ {
168
+ command: "/id",
169
+ adminOnly: true,
170
+ handler: groupHelp,
171
+ triggers: ["/id"],
172
+ description: "Get the group ID.",
173
+ params: {},
174
+ },
166
175
  ],
167
176
  },
168
177
  ];