GPT Middleware
Interact with GPT and use it for your agents.
Is already includes in MessageKit but you could import this one in your files for more customization
src/helpers/gpt.ts
import dotenv from "dotenv";
dotenv.config({ override: true });
import OpenAI from "openai";
import { XMTPContext } from "../lib/xmtp";
import { getUserInfo, replaceUserContext } from "./resolver";
import type { Agent } from "./types";
const isOpenAIConfigured = () => {
return !!process.env.OPENAI_API_KEY;
};
// Modify OpenAI initialization to be conditional
const openai = isOpenAIConfigured()
? new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
: null;
type ChatHistoryEntry = {
role: "user" | "assistant" | "system"; // restrict roles to valid options
content: string;
};
type ChatHistories = Record<string, ChatHistoryEntry[]>;
// New ChatMemory class
class ChatMemory {
private histories: ChatHistories = {};
getHistory(address: string): ChatHistoryEntry[] {
return this.histories[address] || [];
}
addEntry(address: string, entry: ChatHistoryEntry) {
if (!this.histories[address]) {
this.histories[address] = [];
}
this.histories[address].push(entry);
}
initializeWithSystem(address: string, systemPrompt: string) {
if (this.getHistory(address).length === 0) {
this.addEntry(address, {
role: "system",
content: systemPrompt,
});
}
}
clear(address?: string) {
if (address) {
this.histories[address] = [];
} else {
this.histories = {};
}
}
}
// Create singleton instance
export const chatMemory = new ChatMemory();
export const clearMemory = (address?: string) => {
chatMemory.clear(address);
};
export const PROMPT_RULES = `
# Rules
- You can respond with multiple messages if needed. Each message should be separated by a newline character.
- You can trigger skills by only sending the command in a newline message.
- Each command starts with a slash (/).
- Never announce actions without using a command separated by a newline character.
- Never use markdown in your responses.
- Do not make guesses or assumptions
- Only answer if the verified information is in the prompt.
- Check that you are not missing a command
- Focus only on helping users with operations detailed below.
- Date: ${new Date().toUTCString()}
`;
export function replaceSkills(agent: Agent) {
let returnPrompt = `## Commands\n${agent?.skills
.map((skill) => skill.skill + " - " + skill.description)
.join("\n")}\n\n## Examples\n${agent?.skills
.map((skill) => skill.examples?.join("\n"))
.join("\n")}`;
return returnPrompt;
}
export async function replaceVariables(
prompt: string,
senderAddress: string,
agent: Agent,
) {
// Fetch user information based on the sender's address
let userInfo = await getUserInfo(senderAddress);
if (!userInfo) {
console.log("User info not found");
userInfo = {
preferredName: senderAddress,
address: senderAddress,
ensDomain: senderAddress,
converseUsername: senderAddress,
};
}
prompt = prompt.replace(
"{persona}",
"You are a helpful agent called {agent_name} that lives inside a web3 messaging app called Converse.",
);
prompt = prompt.replace("{agent_name}", agent?.tag);
prompt = prompt.replace("{rules}", PROMPT_RULES);
prompt = prompt.replace("{skills}", replaceSkills(agent));
// Replace variables in the system prompt
if (userInfo) {
prompt = prompt.replace("{user_context}", replaceUserContext(userInfo));
prompt = prompt.replaceAll("{address}", userInfo.address || "");
prompt = prompt.replaceAll("{domain}", userInfo.ensDomain || "");
prompt = prompt.replaceAll("{username}", userInfo.converseUsername || "");
prompt = prompt.replaceAll("{name}", userInfo.preferredName || "");
}
if (process.env.MSG_LOG === "true") {
//console.log("System Prompt", prompt);
}
return prompt;
}
export async function agentParse(
prompt: string,
senderAddress: string,
systemPrompt: string,
) {
try {
let userPrompt = prompt;
const userInfo = await getUserInfo(senderAddress);
if (!userInfo) {
console.log("User info not found");
return;
}
const { reply } = await textGeneration(
senderAddress,
userPrompt,
systemPrompt,
);
return reply;
} catch (error) {
console.error("Error during OpenAI call:", error);
throw error;
}
}
export async function agentReply(context: XMTPContext, systemPrompt?: string) {
const {
message: {
content: { text, params },
sender,
},
} = context;
try {
let userPrompt = params?.prompt ?? text;
const { reply } = await textGeneration(
sender.address,
userPrompt,
systemPrompt,
);
await processMultilineResponse(sender.address, reply, context);
} catch (error) {
console.error("Error during OpenAI call:", error);
await context.send("An error occurred while processing your request.");
}
}
export async function textGeneration(
memoryKey: string,
userPrompt: string,
systemPrompt?: string,
) {
// Early validation
if (!openai) {
return { reply: "No OpenAI API key found in .env" };
}
// Handle memory management
if (!memoryKey) clearMemory();
// Initialize or get chat history
chatMemory.initializeWithSystem(memoryKey, systemPrompt ?? "");
let messages = chatMemory.getHistory(memoryKey);
// Add user's prompt
messages.push({ role: "user", content: userPrompt });
try {
// Make OpenAI API call
const response = await openai.chat.completions.create({
model: (process.env.GPT_MODEL as string) || "gpt-4o",
messages: messages,
});
const reply =
response.choices[0].message.content ?? "No response from OpenAI.";
const cleanedReply = parseMarkdown(reply);
// Update chat memory
chatMemory.addEntry(memoryKey, {
role: "assistant",
content: cleanedReply,
});
return { reply: cleanedReply, history: messages };
} catch (error) {
console.error("OpenAI API error:", error);
throw new Error("Failed to generate response");
}
}
export async function processMultilineResponse(
memoryKey: string,
reply: string,
context: XMTPContext,
) {
if (!memoryKey) {
clearMemory();
}
let messages = reply
.split("\n")
.map((message: string) => parseMarkdown(message))
.filter((message): message is string => message.length > 0);
console.log(messages);
for (const message of messages) {
// Check if the message is a command (starts with "/")
if (message.startsWith("/")) {
// Execute the skill associated with the command
const response = await context.executeSkill(message);
if (response && typeof response.message === "string") {
// Parse the response message
let msg = parseMarkdown(response.message);
// Add the parsed message to chat memory as a system message
chatMemory.addEntry(memoryKey, {
role: "system",
content: msg,
});
// Send the response message
await context.send(response.message);
}
} else {
// If the message is not a command, send it as is
await context.send(message);
}
}
}
export function parseMarkdown(message: string) {
let trimmedMessage = message;
// Remove bold and underline markdown
trimmedMessage = trimmedMessage?.replace(/(\*\*|__)(.*?)\1/g, "$2");
// Remove markdown links, keeping only the URL
trimmedMessage = trimmedMessage?.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$2");
// Remove markdown headers
trimmedMessage = trimmedMessage?.replace(/^#+\s*(.*)$/gm, "$1");
// Remove inline code formatting
trimmedMessage = trimmedMessage?.replace(/`([^`]+)`/g, "$1");
// Remove single backticks at the start or end of the message
trimmedMessage = trimmedMessage?.replace(/^`|`$/g, "");
// Remove leading and trailing whitespace
trimmedMessage = trimmedMessage?.replace(/^\s+|\s+$/g, "");
// Remove any remaining leading or trailing whitespace
trimmedMessage = trimmedMessage.trim();
return trimmedMessage;
}