Skip to content

OpenAI middleware

Install dependencies

cmd
yarn add openai

Copy the following code into your lib/openai.ts file.

src/lib/openai.ts
import dotenv from "dotenv";
dotenv.config();
import type { SkillGroup } from "@xmtp/message-kit";
import OpenAI from "openai";
const openai = new OpenAI({
  apiKey: process.env.OPEN_AI_API_KEY,
});
 
export type ChatHistoryEntry = { role: string; content: string };
export 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() {
    this.histories = {};
  }
}
 
export const clearMemory = () => {
  chatHistories = {};
};
 
// Create singleton instance
export const chatMemory = new ChatMemory();
 
let chatHistories: ChatHistories = {};
export const PROMPT_RULES = `You are a helpful and playful agent called {NAME} that lives inside a web3 messaging app called Converse.
- You can respond with multiple messages if needed. Each message should be separated by a newline character.
- You can trigger commands by only sending the command in a newline message.
- Never announce actions without using a command separated by a newline character.
- Dont answer in markdown format, just answer in plaintext.
- 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.
`;
 
export const PROMPT_SKILLS_AND_EXAMPLES = (skills: SkillGroup[]) => `
Commands:
${skills
  .map((skill) => skill.skills.map((s) => s.command).join("\n"))
  .join("\n")}
 
Examples:
${skills
  .map((skill) => skill.skills.map((s) => s.examples).join("\n"))
  .join("\n")}
  `;
 
export async function agentResponse(
  sender: { address: string },
  userPrompt: string,
  systemPrompt: string,
  context: any,
) {
  try {
    const { reply } = await textGeneration(
      sender.address,
      userPrompt,
      systemPrompt,
    );
    await processResponseWithSkill(sender.address, reply, context);
  } catch (error) {
    console.error("Error during OpenAI call:", error);
    await context.reply("An error occurred while processing your request.");
  }
}
export async function textGeneration(
  address: string,
  userPrompt: string,
  systemPrompt: string,
) {
  let messages = chatMemory.getHistory(address);
  chatMemory.initializeWithSystem(address, systemPrompt);
  if (messages.length === 0) {
    messages.push({
      role: "system",
      content: systemPrompt,
    });
  }
  messages.push({
    role: "user",
    content: userPrompt,
  });
  try {
    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: messages as any,
    });
    const reply = response.choices[0].message.content;
    messages.push({
      role: "assistant",
      content: reply || "No response from OpenAI.",
    });
    const cleanedReply = parseMarkdown(reply as string);
    chatMemory.addEntry(address, {
      role: "assistant",
      content: cleanedReply,
    });
    return { reply: cleanedReply, history: messages };
  } catch (error) {
    console.error("Failed to fetch from OpenAI:", error);
    throw error;
  }
}
 
export async function processResponseWithSkill(
  address: string,
  reply: string,
  context: any,
) {
  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) {
    if (message.startsWith("/")) {
      const response = await context.skill(message);
      if (response && typeof response.message === "string") {
        let msg = parseMarkdown(response.message);
        chatMemory.addEntry(address, {
          role: "system",
          content: msg,
        });
        await context.send(response.message);
      }
    } else {
      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;
}