diff --git a/examples/professional/chatgpt-bot/README.md b/examples/professional/chatgpt-bot/README.md new file mode 100644 index 0000000..b3f5529 --- /dev/null +++ b/examples/professional/chatgpt-bot/README.md @@ -0,0 +1,22 @@ +# Wechaty ChatGPT + +This is a wechaty implementation of ChatBot powered by OpenAI ChatGPT + +## Setup + +Modify `dotenv` file to add your openai_api_key as well as the service_token +``` +OPENAI_API_KEY="" +WECHATY_PUPPET_SERVICE_TOKEN="" +``` + +```shell +npm install +mv dotenv .env +``` + +## Run + +```shell +npm run start +``` diff --git a/examples/professional/chatgpt-bot/bot.ts b/examples/professional/chatgpt-bot/bot.ts new file mode 100644 index 0000000..e13cb79 --- /dev/null +++ b/examples/professional/chatgpt-bot/bot.ts @@ -0,0 +1,49 @@ +// Importing the Wechaty npm package +import { Wechaty,WechatyBuilder, Contact, Message, ScanStatus, log } from "wechaty"; +import { Configuration, OpenAIApi, ChatCompletionRequestMessage } from "openai"; +import { PuppetPadlocal } from "wechaty-puppet-padlocal"; +import { onScan, onLogin, onLogout, onMessage, onFriendship } from "./utils"; +import dotenv from "dotenv"; + +dotenv.config(); +console.log(process.env.OPENAI_API_KEY) + + +export function createBot(): Wechaty { + const token: string = process.env.WECHATY_PUPPET_SERVICE_TOKEN! + const puppet = new PuppetPadlocal({ + token, + }); + + return WechatyBuilder.build({ + name: "chatgpt-bot", + puppet, + }); +} + + +// config openAI +const configuration = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); +export const openai = new OpenAIApi(configuration); + +// Initializing the bot +export const bot = createBot(); + +// Keep the conversation state +export const initState: Array = new Array({ "role": "system", "content": "You are a helpful assistant." }) + +bot.on('scan', onScan) +bot.on('login', onLogin) +bot.on('logout', onLogout) +bot.on('message', onMessage) +bot.on('friendship', onFriendship) + +bot.start() + .then(() => log.info('StarterBot', 'Starter Bot Started.')) + .catch(e => log.error('StarterBot', e)) + +bot.ready() + .then(() => log.info('StarterBot', 'Starter Bot Ready.')) + .catch(e => log.error('StarterBot', e)) \ No newline at end of file diff --git a/examples/professional/chatgpt-bot/dotenv b/examples/professional/chatgpt-bot/dotenv new file mode 100644 index 0000000..1434f10 --- /dev/null +++ b/examples/professional/chatgpt-bot/dotenv @@ -0,0 +1,2 @@ +OPENAI_API_KEY="" +WECHATY_PUPPET_SERVICE_TOKEN="" diff --git a/examples/professional/chatgpt-bot/package.json b/examples/professional/chatgpt-bot/package.json new file mode 100644 index 0000000..da622e4 --- /dev/null +++ b/examples/professional/chatgpt-bot/package.json @@ -0,0 +1,30 @@ +{ + "name": "wechaty-chatgpt", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "npx ts-node bot.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^16.0.3", + "file-box": "^1.5.5", + "markdown-it": "^13.0.1", + "node-html-to-image": "^3.3.0", + "openai": "^3.2.1", + "qrcode-terminal": "^0.12.0", + "ts-node": "^10.9.1", + "wechaty": "^1.20.2", + "wechaty-puppet-padlocal": "^1.20.1", + "wechaty-puppet-service": "^1.19.9", + "wechaty-puppet-wechat": "^1.18.4" + }, + "devDependencies": { + "@types/markdown-it": "^12.2.3", + "@types/qrcode-terminal": "^0.12.0" + } +} diff --git a/examples/professional/chatgpt-bot/run.sh b/examples/professional/chatgpt-bot/run.sh new file mode 100755 index 0000000..4aa1329 --- /dev/null +++ b/examples/professional/chatgpt-bot/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +export WECHATY_LOG=verbose +export WECHATY_PUPPET=wechaty-puppet-service + +npm run start diff --git a/examples/professional/chatgpt-bot/utils.ts b/examples/professional/chatgpt-bot/utils.ts new file mode 100644 index 0000000..5ff2d5b --- /dev/null +++ b/examples/professional/chatgpt-bot/utils.ts @@ -0,0 +1,157 @@ +import { Contact, Message, ScanStatus, log, Friendship } from "wechaty"; +import { bot, openai, initState } from "./bot"; +import markdownIt from 'markdown-it'; +import { ChatCompletionRequestMessage } from "openai"; +import { FileBox } from "file-box"; +import qrTerm from "qrcode-terminal"; +import nodeHtmlToImage from 'node-html-to-image'; + +const MEMORY_LIMIT = 50; // max memory +let conversation: Array = new Array(); +conversation.forEach(val => initState.push(Object.assign({}, val))); + +function convertMarkdownToHtml(markdown: string): string { + const md = new markdownIt(); + return md.render(markdown); +} + +export function onScan (qrcode: string, status: ScanStatus) { + if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { + qrTerm.generate(qrcode, { small: true }) // show qrcode on console + + const qrcodeImageUrl = [ + 'https://wechaty.js.org/qrcode/', + encodeURIComponent(qrcode), + ].join('') + + log.info('StarterBot', 'onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) + } else { + log.info('StarterBot', 'onScan: %s(%s)', ScanStatus[status], status) + } + } + +export function onLogin(user: Contact) { + log.info('StarterBot', '%s login', user) +} + +export function onLogout(user: Contact) { + log.info('StarterBot', '%s logout', user) +} + +export async function onMessage(msg: Message) { + log.info('StarterBot', msg.toString()) + + const contact = msg.talker(); + const content = msg.text(); + const isText = msg.type() === bot.Message.Type.Text; + if (msg.self() || !isText) { // msg.self() check if the message is sent from the bot itself + return; + } + if (content === 'ding') { + await contact.say('dong'); + } + // return image + if (content.startsWith("/img ")) { + if (conversation.length === MEMORY_LIMIT) { + // reset to initial state when reach the memory limit + log.info("Resetting memory"); + conversation = new Array(); + conversation.forEach(val => initState.push(Object.assign({}, val))); + } + conversation.push({ "role": "user", "content": content.replace("/i", "") }) + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: conversation, + }); + + try { + const replyContent = response.data.choices[0].message!.content! + const html = convertMarkdownToHtml(replyContent); + await nodeHtmlToImage({ + output: './output.png', + html: html + }) + + log.info('The image was created successfully!') + const fileBox = FileBox.fromFile("./output.png"); + await contact.say(fileBox) + + // record reply + const reply: ChatCompletionRequestMessage = { "role": "assistant", "content": replyContent }; + conversation.push(reply); + } catch (e) { + console.error(e) + } + } else { + // return text if no slash command is specified + if (conversation.length === MEMORY_LIMIT) { + // reset to initial state when reach the memory limit + log.info("Resetting memory"); + conversation = new Array(); + conversation.forEach(val => initState.push(Object.assign({}, val))); + } + conversation.push({ "role": "user", "content": content.replace("/t", "") }) + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: conversation, + }); + + try { + const replyContent = response.data.choices[0].message!.content! + await contact.say(replyContent); + + // record reply + const reply: ChatCompletionRequestMessage = { "role": "assistant", "content": replyContent }; + conversation.push(reply); + } catch (e) { + console.error(e); + } + } +} + +export async function onFriendship(friendship: Friendship) { + let logMsg; + + try { + logMsg = "received `friend` event from " + friendship.contact().name(); + log.info(logMsg); + + switch (friendship.type()) { + /** + * + * 1. New Friend Request + * + * when request is set, we can get verify message from `request.hello`, + * and accept this request by `request.accept()` + */ + + case bot.Friendship.Type.Receive: + logMsg = 'accepted automatically'; + log.info("before accept"); + await friendship.accept(); + + // if want to send msg , you need to delay sometimes + await new Promise((r) => setTimeout(r, 1000)); + await friendship.contact().say(`Hi ${friendship.contact().name()} from FreeChatGPT, I am your person asistant!\n你好 ${friendship.contact().name()} 我是你的私人助理FreeChatGPT!`); + console.log("after accept"); + break; + + /** + * + * 2. Friend Ship Confirmed + * + */ + case bot.Friendship.Type.Confirm: + logMsg = "friendship confirmed with " + friendship.contact().name(); + break; + + default: + break; + } + } catch (e) { + console.error(e); + logMsg = "Friendship try catch failed"; + } + + log.info(logMsg); +} \ No newline at end of file diff --git a/tests/wechaty-puppet-wechat.spec.ts b/tests/wechaty-puppet-wechat.spec.ts index 0c23770..971e65e 100755 --- a/tests/wechaty-puppet-wechat.spec.ts +++ b/tests/wechaty-puppet-wechat.spec.ts @@ -8,7 +8,7 @@ test('wechaty-puppet-wechat', async t => { puppet: 'wechaty-puppet-wechat', puppetOptions: { launchOptions: { - ignoreDefaultArgs: ['--disable-extensions'], + ignoreDefaultArgs: [ '--disable-extensions' ], }, }, } as const