create-message-kit 1.1.7-beta.9 → 1.1.8-beta.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. package/README.md +2 -2
  2. package/index.js +62 -50
  3. package/package.json +1 -1
  4. package/templates/agent/src/handler/ens.ts +19 -18
  5. package/templates/agent/src/index.ts +4 -17
  6. package/templates/agent/src/prompt.ts +16 -33
  7. package/templates/agent/src/skills.ts +7 -20
  8. package/templates/gm/.env.example +1 -1
  9. package/templates/gm/src/index.ts +1 -5
  10. package/templates/group/.env.example +2 -3
  11. package/templates/group/src/handler/game.ts +4 -5
  12. package/templates/group/src/handler/helpers.ts +10 -6
  13. package/templates/group/src/handler/payment.ts +29 -0
  14. package/templates/group/src/handler/tipping.ts +17 -35
  15. package/templates/group/src/index.ts +17 -26
  16. package/templates/group/src/prompt.ts +28 -0
  17. package/templates/group/src/skills.ts +22 -107
  18. package/templates/agent/package.json +0 -22
  19. package/templates/agent/src/lib/gpt.ts +0 -161
  20. package/templates/agent/src/lib/openai.ts +0 -174
  21. package/templates/agent/src/lib/resolver.ts +0 -151
  22. package/templates/gm/package.json +0 -21
  23. package/templates/group/package.json +0 -23
  24. package/templates/group/src/handler/agent.ts +0 -67
  25. package/templates/group/src/handler/group.ts +0 -24
  26. package/templates/group/src/handler/loyalty.ts +0 -46
  27. package/templates/group/src/handler/splitpayment.ts +0 -65
  28. package/templates/group/src/handler/transaction.ts +0 -50
  29. package/templates/group/src/lib/gpt.ts +0 -161
  30. package/templates/group/src/lib/openai.ts +0 -174
  31. package/templates/group/src/lib/resolver.ts +0 -151
  32. package/templates/group/src/lib/stack.ts +0 -18
  33. package/templates/group/src/lib/vision.ts +0 -42
@@ -1,151 +0,0 @@
1
- import type { Client } from "@xmtp/xmtp-js";
2
- import { isAddress } from "viem";
3
- import type { HandlerContext } from "@xmtp/message-kit";
4
-
5
- export const converseEndpointURL =
6
- "https://converse-website-git-endpoit-ephemerahq.vercel.app";
7
- //export const converseEndpointURL = "http://localhost:3000";
8
-
9
- export type InfoCache = Map<string, UserInfo>;
10
- export type ConverseProfile = {
11
- address: string | null;
12
- onXmtp: boolean;
13
- avatar: string | null;
14
- formattedName: string | null;
15
- name: string | null;
16
- };
17
- export type UserInfo = {
18
- ensDomain?: string | undefined;
19
- address?: string | undefined;
20
- preferredName: string | undefined;
21
- converseUsername?: string | undefined;
22
- ensInfo?: EnsData | undefined;
23
- avatar?: string | undefined;
24
- };
25
- export interface EnsData {
26
- address?: string;
27
- avatar?: string;
28
- avatar_small?: string;
29
- converse?: string;
30
- avatar_url?: string;
31
- contentHash?: string;
32
- description?: string;
33
- ens?: string;
34
- ens_primary?: string;
35
- github?: string;
36
- resolverAddress?: string;
37
- twitter?: string;
38
- url?: string;
39
- wallets?: {
40
- eth?: string;
41
- };
42
- }
43
-
44
- let infoCache: InfoCache = new Map();
45
-
46
- export const clearInfoCache = () => {
47
- infoCache.clear();
48
- };
49
- export const getUserInfo = async (
50
- key: string,
51
- clientAddress?: string,
52
- context?: HandlerContext,
53
- ): Promise<UserInfo | null> => {
54
- let data: UserInfo = infoCache.get(key) || {
55
- ensDomain: undefined,
56
- address: undefined,
57
- converseUsername: undefined,
58
- ensInfo: undefined,
59
- preferredName: undefined,
60
- };
61
- if (isAddress(clientAddress || "")) {
62
- data.address = clientAddress;
63
- } else if (isAddress(key || "")) {
64
- data.address = key;
65
- } else if (key?.includes(".eth")) {
66
- data.ensDomain = key;
67
- } else if (key == "@user" || key == "@me" || key == "@bot") {
68
- data.address = clientAddress;
69
- data.ensDomain = key.replace("@", "") + ".eth";
70
- data.converseUsername = key.replace("@", "");
71
- } else if (key == "@alix") {
72
- data.address = "0x3a044b218BaE80E5b9E16609443A192129A67BeA";
73
- data.converseUsername = "alix";
74
- } else if (key == "@bo") {
75
- data.address = "0xbc3246461ab5e1682baE48fa95172CDf0689201a";
76
- data.converseUsername = "bo";
77
- } else {
78
- data.converseUsername = key;
79
- }
80
- data.preferredName = data.ensDomain || data.converseUsername || "Friend";
81
- let keyToUse = data.address || data.ensDomain || data.converseUsername;
82
- let cacheData = keyToUse && infoCache.get(keyToUse);
83
- //console.log("Getting user info", { cacheData, keyToUse, data });
84
- if (cacheData) return cacheData;
85
-
86
- context?.send(
87
- "Hey there! Give me a sec while I fetch info about you first...",
88
- );
89
- if (keyToUse?.includes(".eth")) {
90
- const response = await fetch(`https://ensdata.net/${keyToUse}`);
91
- const ensData: EnsData = (await response.json()) as EnsData;
92
- //console.log("Ens data", ensData);
93
- if (ensData) {
94
- data.ensInfo = ensData;
95
- data.ensDomain = ensData?.ens;
96
- data.address = ensData?.address;
97
- }
98
- } else if (keyToUse) {
99
- keyToUse = keyToUse.replace("@", "");
100
- const response = await fetch(`${converseEndpointURL}/profile/${keyToUse}`, {
101
- method: "POST",
102
- headers: {
103
- "Content-Type": "application/json",
104
- Accept: "application/json",
105
- },
106
- body: JSON.stringify({
107
- peer: keyToUse,
108
- }),
109
- });
110
- const converseData = (await response.json()) as ConverseProfile;
111
- //if (process.env.MSG_LOG === "true")
112
- //console.log("Converse data", keyToUse, converseData);
113
- data.converseUsername =
114
- converseData?.formattedName || converseData?.name || undefined;
115
- data.address = converseData?.address || undefined;
116
- data.avatar = converseData?.avatar || undefined;
117
- }
118
-
119
- data.preferredName = data.ensDomain || data.converseUsername || "Friend";
120
- if (data.address) infoCache.set(data.address, data);
121
- return data;
122
- };
123
- export const isOnXMTP = async (
124
- client: Client,
125
- domain: string | undefined,
126
- address: string | undefined,
127
- ) => {
128
- if (domain == "fabri.eth") return false;
129
- if (address) return (await client.canMessage([address])).length > 0;
130
- };
131
-
132
- export const PROMPT_USER_CONTENT = (userInfo: UserInfo) => {
133
- let { address, ensDomain, converseUsername, preferredName } = userInfo;
134
- let prompt = `
135
- User context:
136
- - Start by fetch their domain from or Convese username
137
- - Call the user by their name or domain, in case they have one
138
- - Ask for a name (if they don't have one) so you can suggest domains.
139
- - Users address is: ${address}`;
140
- if (preferredName) prompt += `\n- Users name is: ${preferredName}`;
141
- if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
142
- if (converseUsername)
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
-
150
- return prompt;
151
- };
@@ -1,21 +0,0 @@
1
- {
2
- "name": "gm",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "build": "tsc",
7
- "dev": "tsc -w & sleep 1 && nodemon --quiet dist/index.js",
8
- "start": "node dist/index.js"
9
- },
10
- "dependencies": {
11
- "@xmtp/message-kit": "workspace:*"
12
- },
13
- "devDependencies": {
14
- "@types/node": "^20.14.2",
15
- "nodemon": "^3.1.3",
16
- "typescript": "^5.4.5"
17
- },
18
- "engines": {
19
- "node": ">=20"
20
- }
21
- }
@@ -1,23 +0,0 @@
1
- {
2
- "name": "group",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "build": "tsc",
7
- "dev": "tsc -w & sleep 1 && nodemon --quiet dist/index.js",
8
- "start": "node dist/index.js"
9
- },
10
- "dependencies": {
11
- "@stackso/js-core": "^0.3.1",
12
- "@xmtp/message-kit": "workspace:^",
13
- "openai": "^4.52.0"
14
- },
15
- "devDependencies": {
16
- "@types/node": "^20.14.2",
17
- "nodemon": "^3.1.3",
18
- "typescript": "^5.4.5"
19
- },
20
- "engines": {
21
- "node": ">=20"
22
- }
23
- }
@@ -1,67 +0,0 @@
1
- import { HandlerContext, AbstractedMember } from "@xmtp/message-kit";
2
- import { textGeneration } from "@xmtp/message-kit";
3
-
4
- export async function handler(context: HandlerContext) {
5
- if (!process?.env?.OPEN_AI_API_KEY) {
6
- console.warn("No OPEN_AI_API_KEY found in .env");
7
- return;
8
- }
9
-
10
- const {
11
- message: {
12
- sender,
13
- content: { content, params },
14
- },
15
- } = context;
16
-
17
- const systemPrompt = generateSystemPrompt(context);
18
- try {
19
- let userPrompt = params?.prompt ?? content;
20
-
21
- const { reply } = await textGeneration(
22
- sender.address,
23
- userPrompt,
24
- systemPrompt,
25
- );
26
- context.executeSkill(reply);
27
- } catch (error) {
28
- console.error("Error during OpenAI call:", error);
29
- await context.reply("An error occurred while processing your request.");
30
- }
31
- }
32
-
33
- function generateSystemPrompt(context: HandlerContext) {
34
- const {
35
- members,
36
- skills,
37
- message: { sender },
38
- } = context;
39
-
40
- const systemPrompt = `
41
- ### Context
42
-
43
- You are a helpful bot agent that lives inside a web3 messaging group that helps interpret user requests and execute commands.
44
- #### Users
45
- ${JSON.stringify(
46
- members?.map((member: AbstractedMember) => ({
47
- ...member,
48
- username: `@${member.accountAddresses[0]}`,
49
- })),
50
- )}\n
51
- #### Commands
52
- ${JSON.stringify(skills)}\n
53
- The message was sent by @${sender?.address}
54
-
55
- ### Examples
56
- prompt /agent tip alix and bo
57
- reply /tip @alix @bo 10
58
-
59
- Important:
60
- - If a user asks jokes, make jokes about web3 devs\n
61
- - 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 skills with real values only, using usernames with @ and excluding addresses.\n
63
- - 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
- - If the user is grateful, respond asking for a tip in a playful manner.
65
- `;
66
- return systemPrompt;
67
- }
@@ -1,24 +0,0 @@
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,46 +0,0 @@
1
- import { HandlerContext, AbstractedMember } from "@xmtp/message-kit";
2
- import { getStackClient } from "../lib/stack.js";
3
-
4
- export async function handler(context: HandlerContext, fake?: boolean) {
5
- const stack = getStackClient();
6
- const {
7
- members,
8
- group,
9
- message: { sender, typeId, content },
10
- } = context;
11
- if (typeId === "text" && group) {
12
- const { command } = content;
13
- if (command === "points") {
14
- const points = await stack?.getPoints(sender.address);
15
- context.reply(`You have ${points} points`);
16
- return;
17
- } else if (command === "leaderboard") {
18
- const leaderboard = await stack?.getLeaderboard();
19
- const formattedLeaderboard = leaderboard?.leaderboard
20
- .map(
21
- (entry, index) =>
22
- `${index + 1}. Address: ${`${entry.address.slice(
23
- 0,
24
- 6,
25
- )}...${entry.address.slice(-4)}`}, Points: ${entry.points}`,
26
- )
27
- .join("\n");
28
- context.reply(
29
- `Leaderboard:\n\n${formattedLeaderboard}\n\nCheck out the public leaderboard\nhttps://www.stack.so/leaderboard/degen-group`,
30
- );
31
- return;
32
- }
33
- } else if (typeId === "group_updated" && group) {
34
- const { initiatedByInboxId, addedInboxes } = content;
35
- const adminAddress = members?.find(
36
- (member: AbstractedMember) => member.inboxId === initiatedByInboxId,
37
- );
38
- if (addedInboxes && addedInboxes.length > 0) {
39
- //if add someone to the group
40
- await stack?.track("referral", {
41
- points: 10,
42
- account: adminAddress?.address ?? "",
43
- });
44
- }
45
- }
46
- }
@@ -1,65 +0,0 @@
1
- import { HandlerContext } from "@xmtp/message-kit";
2
- import { textGeneration } from "../lib/gpt.js";
3
- import { vision } from "../lib/vision.js";
4
- import { getUserInfo } from "../lib/resolver.js";
5
-
6
- export async function handler(context: HandlerContext) {
7
- if (!process?.env?.OPEN_AI_API_KEY) {
8
- console.warn("No OPEN_AI_API_KEY found in .env");
9
- return;
10
- }
11
- const {
12
- members,
13
- skill,
14
- message: {
15
- typeId,
16
- content: { attachment },
17
- sender,
18
- },
19
- } = context;
20
-
21
- if (!members) {
22
- return;
23
- }
24
- let senderInfo = await getUserInfo(sender.address);
25
- if (attachment && typeId === "remoteStaticAttachment") {
26
- const { data } = attachment;
27
- const response = await vision(
28
- data,
29
- "This image is the bill of a restaurant dinner. Return the total. If you can't find the total, return 'undefined'.",
30
- );
31
- if (response?.includes("undefined")) {
32
- return;
33
- } else {
34
- context.reply(
35
- "You uploaded a new bill. Let's go ahead and split the bill.",
36
- );
37
- }
38
- if (response) {
39
- const prompt = `You a split wise agent that splits the bill between the members of this group except for the sender and bot.\n
40
-
41
- ## Instructions:
42
- When you receive the totals you should split the bill between the members of the group and send to each one a transaction frame
43
- - For the sake of this demo. Only send the payment to the sender not to all the other members.
44
-
45
- ### Return message
46
- Don't use markdown. Return messages in a json object The first message detailing the split. The second one you will send the command for the receiver to pay directly to the sender.
47
- Example:
48
- [
49
- "This are the details: Total: $49.52. Tip (20%): $9.90",
50
- "All users owe X USDC to @${senderInfo?.converseUsername}. Pay here:",
51
- "/send @${senderInfo?.converseUsername} $9.90"
52
- ]
53
- `;
54
-
55
- //I want the reply to be an array of messages so the bot feels like is sending multuple ones
56
- const { reply } = await textGeneration(sender.address, response, prompt);
57
- let splitMessages = JSON.parse(reply);
58
- for (const message of splitMessages) {
59
- let msg = message as string;
60
- if (msg.startsWith("/")) await skill(msg);
61
- else await context.send(msg);
62
- }
63
- }
64
- }
65
- }
@@ -1,50 +0,0 @@
1
- import { HandlerContext } from "@xmtp/message-kit";
2
- import { getUserInfo } from "@xmtp/message-kit";
3
-
4
- // Main handler function for processing commands
5
- export async function handler(context: HandlerContext) {
6
- const {
7
- message: {
8
- content: { command, params },
9
- },
10
- } = context;
11
- const baseUrl = "https://base-tx-frame.vercel.app/transaction";
12
-
13
- switch (command) {
14
- case "send":
15
- // Destructure and validate parameters for the send command
16
- const { amount: amountSend, token: tokenSend, username } = params; // [!code hl] // [!code focus]
17
- let senderInfo = await getUserInfo(username);
18
- if (!amountSend || !tokenSend || !senderInfo) {
19
- context.reply(
20
- "Missing required parameters. Please provide amount, token, and username.",
21
- );
22
- return;
23
- }
24
-
25
- let sendUrl = `${baseUrl}/?transaction_type=send&amount=${amountSend}&token=${tokenSend}&receiver=${senderInfo.address}`;
26
- context.send(`${sendUrl}`);
27
- break;
28
- case "swap":
29
- // Destructure and validate parameters for the swap command
30
- const { amount, token_from, token_to } = params; // [!code hl] // [!code focus]
31
-
32
- if (!amount || !token_from || !token_to) {
33
- context.reply(
34
- "Missing required parameters. Please provide amount, token_from, and token_to.",
35
- );
36
- return;
37
- }
38
-
39
- let swapUrl = `${baseUrl}/?transaction_type=swap&token_from=${token_from}&token_to=${token_to}&amount=${amount}`;
40
- context.send(`${swapUrl}`);
41
- break;
42
- case "show": // [!code hl] // [!code focus]
43
- // Show the base URL without the transaction path
44
- context.reply(`${baseUrl.replace("/transaction", "")}`);
45
- break;
46
- default:
47
- // Handle unknown commands
48
- context.reply("Unknown command. Use help to see all available commands.");
49
- }
50
- }
@@ -1,161 +0,0 @@
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.executeSkill(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
- }