Ethora Community

Community › Developers (platform specific questions) › Chat Bots (Node.js, Python)

Using Cursor.ai to create a Typescript OpenAI powered AI bot for Ethora

Archived from the Ethora community forum. First posted on 10 February 2025; product details may have changed since. For current setup see Get Ethora and GitHub.

Q: how do I create a chat bot that connects LLM to one of my chat rooms here?

A: I'll help you create a chat bot that connects an LLM to your XMPP chat rooms. Here's how you can implement it:

 
 
  • First, create a new bot client class:
// src/bots/LLMBot.ts
import { Client, xml } from '@xmpp/client';
import { OpenAI } from 'openai';

export class LLMBot {
  private xmppClient: Client;
  private openai: OpenAI;
  private roomJID: string;

  constructor(config: {
    botJID: string,
    password: string,
    roomJID: string,
    openaiApiKey: string
  }) {
    this.roomJID = config.roomJID;
    this.openai = new OpenAI({ apiKey: config.openaiApiKey });
    
    // Initialize XMPP client for the bot
    this.xmppClient = new Client({
      service: `wss://${VITE_APP_XMPP_BASEDOMAIN}:5443/ws`,
      domain: VITE_APP_XMPP_BASEDOMAIN,
      resource: 'bot',
      username: config.botJID,
      password: config.password
    });

    this.setupListeners();
  }

  private async setupListeners() {
    // Handle incoming messages
    this.xmppClient.on('stanza', async (stanza) => {
      if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
        const body = stanza.getChild('body')?.text();
        if (body && !stanza.getChild('delay')) {
          await this.handleMessage(body);
        }
      }
    });

    // Handle errors
    this.xmppClient.on('error', (err) => {
      console.error('Bot XMPP error:', err);
    });
  }

  private async handleMessage(message: string) {
    try {
      // Get response from OpenAI
      const completion = await this.openai.chat.completions.create({
        messages: [{ role: 'user', content: message }],
        model: 'gpt-3.5-turbo',
      });

      const response = completion.choices[0].message.content;

      // Send response back to the room
      if (response) {
        this.sendMessage(response);
      }
    } catch (error) {
      console.error('Error getting LLM response:', error);
    }
  }

  private sendMessage(message: string) {
    const stanza = xml(
      'message',
      { to: this.roomJID, type: 'groupchat' },
      xml('body', {}, message)
    );
    this.xmppClient.send(stanza);
  }

  public async connect() {
    await this.xmppClient.start();
    // Join the room
    const presence = xml(
      'presence',
      { to: `${this.roomJID}/LLMBot` }
    );
    this.xmppClient.send(presence);
  }
}

Create a hook to manage the bot:

// src/hooks/useLLMBot.ts
import { useEffect, useRef } from 'react';
import { LLMBot } from '../bots/LLMBot';

export const useLLMBot = (roomJID: string) => {
  const botRef = useRef<LLMBot | null>(null);

  useEffect(() => {
    const initBot = async () => {
      const bot = new LLMBot({
        botJID: 'llmbot',  // Your bot's JID
        password: 'botpassword', // Your bot's password
        roomJID,
        openaiApiKey: import.meta.env.VITE_OPENAI_API_KEY
      });

      await bot.connect();
      botRef.current = bot;
    };

    initBot();

    return () => {
      if (botRef.current) {
        // Cleanup if needed
      }
    };
  }, [roomJID]);

  return botRef.current;
};

Integrate the bot with your ChatWrapper component. Add this to the existing ChatWrapper component (reference lines 137-208 for context):

// In ChatWrapper.tsx
import { useLLMBot } from '../../hooks/useLLMBot';

// Add inside the component:
const bot = useLLMBot(roomJID);

Add the OpenAI API key to your environment variables:
 

# .env
VITE_OPENAI_API_KEY=your_openai_api_key_here

To use this bot:

 
 
  • Install required dependencies:
npm install @xmpp/client openai
  • Create a bot account on your XMPP server
 
 
  • Update the bot credentials in the useLLMBot hook
 
 
  • The bot will automatically:
 
 
  • Connect to the specified room
 
 
  • Listen for messages
 
 
  • Generate responses using the LLM
 
 
  • Send responses back to the room
 
 

The bot will process all messages in the room and respond using the OpenAI API. You can customize the prompt, model, and response handling in the handleMessage method of the LLMBot class.

 

N.B. (TF): here I skip attempt building it in Python and returned to Typescript

 

Let's create a TypeScript version of the XMPP bot based on your JavaScript implementation (referencing src/networking/xmppClient.ts lines 28-82 for the client setup). First, let's create the bot:

// src/bot.ts
import { client, xml } from '@xmpp/client'
import debug from '@xmpp/debug'
import dotenv from 'dotenv'

interface BotConfig {
  jid: string
  password: string
  roomJid: string
  openaiKey: string
}

class EthoraBot {
  private client
  private roomJid: string
  private openaiKey: string
  
  constructor(config: BotConfig) {
    this.roomJid = config.roomJid
    this.openaiKey = config.openaiKey
    
    // Initialize XMPP client
    this.client = client({
      service: 'wss://xmpp.ethoradev.com:5443/ws',
      username: config.jid,
      password: config.password,
    })

    // Add event handlers
    this.client.on('online', () => this.onOnline())
    this.client.on('error', (err) => console.error('❌ Error:', err))
    this.client.on('stanza', (stanza) => this.onStanza(stanza))
    
    // Debug output
    debug(this.client, true)
  }

  async start() {
    try {
      await this.client.start()
      console.log('🟢 Bot connected')
    } catch (err) {
      console.error('Failed to start:', err)
    }
  }

  private async onOnline() {
    console.log('🟢 Bot is online')
    
    // Send initial presence
    const presence = xml(
      'presence',
      { to: `${this.roomJid}/${this.client.jid!.local}` },
      xml('x', { xmlns: 'http://jabber.org/protocol/muc' })
    )
    
    await this.client.send(presence)
    console.log('🎯 Joined room:', this.roomJid)
    
    // Send test message
    const message = xml(
      'message',
      { 
        to: this.roomJid,
        type: 'groupchat',
      },
      xml('body', {}, 'Bot initialization test'),
      xml('data', { 
        xmlns: 'jabber:client',
        fullName: 'AI Assistant',
        showInChannel: 'true'
      })
    )
    
    await this.client.send(message)
  }

  private async onStanza(stanza: any) {
    if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
      const body = stanza.getChild('body')
      if (body) {
        console.log('📩 Message:', body.text())
        // Handle message here
      }
    }
  }
}

// Initialize bot
dotenv.config()
const bot = new EthoraBot({
  jid: process.env.BOT_JID!,
  password: process.env.BOT_PASSWORD!,
  roomJid: process.env.ROOM_JID!,
  openaiKey: process.env.OPENAI_API_KEY!
})

bot.start()

And create a package.json:

{
  "name": "ethora-bot",
  "version": "1.0.0",
  "main": "dist/bot.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/bot.js",
    "dev": "ts-node src/bot.ts"
  },
  "dependencies": {
    "@xmpp/client": "^0.13.1",
    "@xmpp/debug": "^0.13.0",
    "dotenv": "^16.0.3"
  },
  "devDependencies": {
    "@types/node": "^18.0.0",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.5"
  }
}

 

This implementation:

 
 
  • Uses the same WebSocket connection as your JavaScript client
 
 
  • Follows the same presence and message format from your XML examples (referencing lines 31-36)
 
 
  • Includes proper error handling and logging
 
 
  • Uses TypeScript for better type safety
 
 

To run it:

 
 
  • npm install
 
 
  • Create .env with the same credentials as before
 
 
  • npm run dev

 

the file structure needs to be corrected. Let's organize the files properly:

 
 
  • First, create this directory structure from the /src folder:

src/
  ├── bot/
   │         └── bot.ts
  ├── package.json
  ├── tsconfig.json
  └── .env

  • Move the bot code to src/bot/bot.ts (same code as before)
 
 
  • Create a tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["bot/**/*"],
  "exclude": ["node_modules", "dist"]
}

Update the package.json scripts (referencing lines 3-7 from your package.json):

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/bot/bot.js",
    "dev": "ts-node bot/bot.ts"
  }
}

 

The rest of package.json and .env files remain the same.

 
 

Now you should be able to run:

cd src
npm install
npm run dev

Let's fix the TypeScript errors. First, let's create type declarations and update the bot code:

 
 
  • Create a new file src/types/xmpp.d.ts:
declare module '@xmpp/client' {
  export function client(config: {
    service: string;
    username: string;
    password: string;
  }): any;
  
  export function xml(
    name: string,
    attrs?: Record<string, any>,
    ...children: any[]
  ): any;
}

declare module '@xmpp/debug' {
  export default function debug(client: any, enabled: boolean): void;
}

Update tsconfig.json to include the types:

 

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "typeRoots": ["./types", "./node_modules/@types"]
  },
  "include": ["bot/**/*", "types/**/*"],
  "exclude": ["node_modules", "dist"]
}

 

This should resolve the TypeScript errors while maintaining the same functionality. The code matches our JavaScript client's implementation (referencing src/networking/xmppClient.ts lines 69-73 for client configuration).

 

TF: skipped some debugging, OpenAI and added ability to handle chat history.

Encountered TypeScript type issues with the OpenAI API. Let's fix the types and message handling:

import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'

class EthoraBot {
  private client: any
  private roomJid: string
  private openaiKey: string
  private openai: OpenAI
  private chatHistory: ChatCompletionMessageParam[] = []
  
  constructor(config: BotConfig) {
    // ... existing constructor code ...

    // Initialize chat history with system message
    this.chatHistory = [{
      role: "system",
      content: "You are a helpful AI assistant in a group chat. Keep responses concise and friendly."
    } as ChatCompletionMessageParam]
  }

  private async onStanza(stanza: XMPPStanza) {
    if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
      const body = stanza.getChild('body')
      if (body) {
        const messageText = body.text()
        
        // Don't respond to our own messages
        const from = stanza.attrs.from
        if (from.includes(this.client.jid!.local)) {
          return
        }

        try {
          // Add user message to history
          this.chatHistory.push({
            role: "user",
            content: messageText
          } as ChatCompletionMessageParam)

          // Keep history limited to last 10 messages
          if (this.chatHistory.length > 11) {
            this.chatHistory = [
              this.chatHistory[0],
              ...this.chatHistory.slice(-10)
            ]
          }

          // Generate AI response with history
          const completion = await this.openai.chat.completions.create({
            messages: this.chatHistory,
            model: "gpt-3.5-turbo",
            temperature: 0.7
          })

          const response = completion.choices[0].message.content

          if (response) {
            // Add AI response to history
            this.chatHistory.push({
              role: "assistant",
              content: response
            } as ChatCompletionMessageParam)

            // Send response with nickname
            const message = xml(
              'message',
              { 
                to: this.roomJid,
                type: 'groupchat',
              },
              xml('body', {}, response),
              xml('data', { 
                xmlns: 'jabber:client',
                fullName: 'Ethora AI',
                senderFirstName: 'Ethora',
                senderLastName: 'AI',
                showInChannel: 'true'
              })
            )
            
            await this.client.send(message)
          }

        } catch (error) {
          console.error('Error generating response:', error)
        }
      }
    }
  }
}

Changes done:

 
 
  • Added proper OpenAI types for chat messages
 
 
  • Added type assertions for message objects
 
 
  • Added null check for response
 
 
  • Added temperature parameter to make responses more consistent
 

Another couple problems after testing this - bot's name has disappeared in chat, also it's not responding now, but it sends the first test message.

Let's fix the bot's name and response issues. Looking at your code, there are a few things to adjust:

 
 
  • First, let's fix the initial presence message (referencing src/bot/bot.ts lines 71-77). The nickname needs to be set differently:

 

const presence = xml(
  'presence',
  { to: `${this.roomJid}/${this.client.jid!.local}` },
  xml('x', { xmlns: 'http://jabber.org/protocol/muc' }),
  xml('data', { 
    xmlns: 'jabber:client',
    fullName: 'Ethora AI',
    senderFirstName: 'Ethora',
    senderLastName: 'AI',
    showInChannel: 'true'
  })
)

For the test message (referencing src/bot/bot.ts lines 83-95), update the data attributes:

const message = xml(
  'message',
  { 
    to: this.roomJid,
    type: 'groupchat',
  },
  xml('body', {}, 'Bot initialization test'),
  xml('data', { 
    xmlns: 'jabber:client',
    fullName: 'Ethora AI',
    senderFirstName: 'Ethora',
    senderLastName: 'AI',
    showInChannel: 'true'
  })
)

For debugging the response issue, add some console logs in the onStanza method (referencing src/bot/bot.ts lines 100-110):

private async onStanza(stanza: XMPPStanza) {
  if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
    const body = stanza.getChild('body')
    if (body) {
      const messageText = body.text()
      console.log('📩 Received message:', messageText)
      console.log('📤 From:', stanza.attrs.from)
      console.log('🤖 Bot JID:', this.client.jid!.local)
      
      // Don't respond to our own messages
      const from = stanza.attrs.from
      if (from.includes(this.client.jid!.local)) {
        console.log('⏭️ Skipping own message')
        return
      }
      // ... rest of the method

These changes:

1. Fix the bot's name display in chat by using the correct XMPP attributes
2. Debugs bot responsiveness by adding detailed logging
3. Maintain consistency with our XMPP message format (referencing src/assets/test.xml lines 39-46)

Note the .env file format:

BOT_JID=
BOT_PASSWORD=
ROOM_JID=something@conference.xmpp.ethoradev.com
OPENAI_API_KEY=

 

ethora_ai_chat_bot_public.zipUnavailable

Have a question about this? Ask the Ethora team.