create-message-kit 1.0.20 → 1.1.5-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +25 -22
- package/package.json +1 -1
- package/templates/agent/src/handler/ens.ts +5 -41
- package/templates/agent/src/index.ts +31 -2
- package/templates/agent/src/lib/openai.ts +90 -56
- package/templates/agent/src/lib/resolver.ts +18 -8
- package/templates/agent/src/prompt.ts +16 -29
- package/templates/agent/src/skills.ts +9 -9
- package/templates/group/src/handler/splitpayment.ts +3 -7
- package/templates/group/src/lib/openai.ts +90 -55
- package/templates/group/src/lib/resolver.ts +18 -8
- package/templates/group/src/lib/vision.ts +43 -0
- package/templates/group/src/skills.ts +7 -0
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
|
16
|
+
log.info(pc.cyan(`Welcome to MessageKit!`));
|
23
17
|
const coolLogo = `
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
18
|
+
███╗ ███╗███████╗███████╗███████╗ █████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗
|
19
|
+
████╗ ████║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██╔╝██║╚══██╔══╝
|
20
|
+
██╔████╔██║█████╗ ███████╗███████╗███████║██║ ███╗█████╗ █████╔╝ ██║ ██║
|
21
|
+
██║╚██╔╝██║██╔══╝ ╚════██║╚════██║██╔══██║██║ ██║██╔══╝ ██╔═██╗ ██║ ██║
|
22
|
+
██║ ╚═╝ ██║███████╗███████║███████║██║ ██║╚██████╔╝███████╗██║ ██╗██║ ██║
|
23
|
+
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
24
|
+
Powered by XMTP`;
|
31
25
|
|
32
|
-
log.info(pc.
|
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,
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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,
|
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,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 {
|
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
|
-
|
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
|
-
|
173
|
+
clearMemory();
|
210
174
|
clearInfoCache();
|
211
175
|
}
|
@@ -1,10 +1,39 @@
|
|
1
1
|
import { run, HandlerContext } from "@xmtp/message-kit";
|
2
|
-
import {
|
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
|
-
|
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 =
|
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 =
|
40
|
-
|
41
|
-
|
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) =>
|
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 =
|
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
|
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
|
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
|
-
|
81
|
-
|
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
|
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
|
5
|
-
let { address, ensDomain, converseUsername } = userInfo;
|
5
|
+
export async function agent_prompt(userInfo: UserInfo) {
|
6
|
+
let { address, ensDomain, converseUsername, preferredName } = userInfo;
|
6
7
|
|
7
|
-
|
8
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
22
|
-
|
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 ${
|
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 ${
|
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
|
66
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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]
|
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 {
|
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 =
|
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 =
|
40
|
-
|
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) =>
|
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 =
|
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
|
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
|
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
|
-
|
81
|
-
|
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: {},
|