mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-13 23:16:55 +00:00
Implement Discord bot starter template (#129)
# Pull Request Description ## Changes Made This PR introduces a starter template example project for creating a Discord Bot built to interact with the user and perform actions using the Solana Agent Kit. ## Implementation Details The bot uses the latest version of discord.js and has an ability to process DMs on Discord, perform actions using the agent kit and reply back to the user. [Link to demo video](https://bafybeidjub6alzloxx2on5df6j5dhfse7txemivcbjohxzlqkxuz5ymqlm.ipfs.web3approved.com/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaWQiOiJiYWZ5YmVpZGp1YjZhbHpsb3h4Mm9uNWRmNmo1ZGhmc2U3dHhlbWl2Y2Jqb2h4emxxa3h1ejV5bXFsbSIsInByb2plY3RfdXVpZCI6IjRlOTEzNjIzLTI0N2ItNDAwMC1iNjUwLTgyNzI0MzBhMzk3MCIsImlhdCI6MTczNTk4Njc5Niwic3ViIjoiSVBGUy10b2tlbiJ9.vF6R99GnycpALTbPPmVJUXWvqXHYGkhfBIMaF1wVvkk) ## Checklist - [x] I have tested these changes locally - [x] I have updated the documentation - [x] I have added a transaction link - [x] I have added the prompt used to test it
This commit is contained in:
4
examples/discord-bot-starter/.env.template
Normal file
4
examples/discord-bot-starter/.env.template
Normal file
@@ -0,0 +1,4 @@
|
||||
DISCORD_BOT_TOKEN=
|
||||
SOLANA_PRIVATE_KEY=
|
||||
SOLANA_RPC_URL=
|
||||
OPENAI_API_KEY=
|
||||
123
examples/discord-bot-starter/.eslintrc.js
Normal file
123
examples/discord-bot-starter/.eslintrc.js
Normal file
@@ -0,0 +1,123 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
root: true,
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
'.eslintrc.js',
|
||||
'webpack.config.js',
|
||||
'dist/*',
|
||||
'**/*.js',
|
||||
'node_modules/*',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: '.',
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:sonarjs/recommended',
|
||||
'plugin:security/recommended',
|
||||
'plugin:promise/recommended',
|
||||
'prettier',
|
||||
],
|
||||
plugins: [
|
||||
'@typescript-eslint/eslint-plugin',
|
||||
'sonarjs',
|
||||
'security',
|
||||
'promise',
|
||||
'prettier',
|
||||
],
|
||||
rules: {
|
||||
semi: [2, 'always'],
|
||||
quotes: [1, 'single', { allowTemplateLiterals: true }],
|
||||
curly: [2, 'all'],
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
'error',
|
||||
{ checksVoidReturn: false },
|
||||
],
|
||||
'security/detect-non-literal-regexp': 0,
|
||||
'security/detect-object-injection': 0,
|
||||
'promise/always-return': 0,
|
||||
'promise/no-callback-in-promise': 0,
|
||||
'sonarjs/cognitive-complexity': [2, 50],
|
||||
'sonarjs/no-duplicate-string': 0,
|
||||
'sonarjs/no-useless-catch': 1,
|
||||
'sonarjs/no-nested-template-literals': 0,
|
||||
'sonarjs/prefer-single-boolean-return': 1,
|
||||
'sonarjs/no-small-switch': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
1,
|
||||
{ argsIgnorePattern: '^_|^returns$|^of$|^type$' },
|
||||
],
|
||||
'import/extensions': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/no-dynamic-require': 'off',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
useTabs: false,
|
||||
arrowParens: 'always',
|
||||
printWidth: 80,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
endOfLine: 'auto',
|
||||
bracketSpacing: true,
|
||||
},
|
||||
{
|
||||
usePrettierrc: false,
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['**/dist/**'],
|
||||
},
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'no-console': 'off',
|
||||
'no-return-await': 'off',
|
||||
'consistent-return': 'off',
|
||||
'default-case': 'off',
|
||||
'no-fallthrough': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'no-void': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
'no-return-assign': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'global-require': 'off',
|
||||
'security/detect-non-literal-require': 'off',
|
||||
'global-require': 'off',
|
||||
},
|
||||
};
|
||||
8
examples/discord-bot-starter/.gitignore
vendored
Normal file
8
examples/discord-bot-starter/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
logs/
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
10
examples/discord-bot-starter/.prettierrc
Normal file
10
examples/discord-bot-starter/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "auto",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
41
examples/discord-bot-starter/README.md
Normal file
41
examples/discord-bot-starter/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Discord Bot Starter
|
||||
|
||||
This is a starter template for creating a Discord bot using the Solana Agent Kit by Send AI.
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v20 or higher)
|
||||
- pnpm (v9 or higher)
|
||||
- A Discord account
|
||||
- A Solana account keypair
|
||||
|
||||
### Step 1: Create a Discord Bot
|
||||
|
||||
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications).
|
||||
2. Click on "New Application" and give your application a name.
|
||||
3. Navigate to the "Bot" tab on the left sidebar and click "Add Bot".
|
||||
4. Under the "Token" section, click "Copy" to copy your bot token.
|
||||
|
||||
### Step 2: Fill Out Environment Variables
|
||||
|
||||
Create a `.env` file in the root directory of the project and fill it out with the following variables:
|
||||
|
||||
- `DISCORD_BOT_TOKEN`: Paste the bot token you copied from the Discord Developer Portal.
|
||||
- `SOLANA_PRIVATE_KEY`: Enter your Solana private key. This is required for interacting with the Solana blockchain.
|
||||
- `SOLANA_RPC_URL`: Provide the RPC URL for connecting to the Solana network. You can use a public RPC URL or your own.
|
||||
- `OPENAI_API_KEY`: Input your OpenAI API key if you plan to use OpenAI services within your bot. You can obtain this key from the OpenAI platform.
|
||||
|
||||
### Step 3: Install Dependencies and Start the Bot
|
||||
|
||||
1. Open a terminal and navigate to the root directory of the project.
|
||||
2. Run the following command to install the project dependencies:
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
3. After the installation is complete, start the bot by running:
|
||||
```sh
|
||||
pnpm start
|
||||
```
|
||||
4. Once the bot is running, open Discord and send a direct message (DM) to your bot to ensure it is working correctly.
|
||||
39
examples/discord-bot-starter/package.json
Normal file
39
examples/discord-bot-starter/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "discord-bot-starter",
|
||||
"version": "1.0.0",
|
||||
"description": "Discord bot starter template using the Solana Agent Kit by Send AI",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "nodemon ./src/index.ts",
|
||||
"lint": "eslint -c .eslintrc.js --ext .ts ./src",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
|
||||
},
|
||||
"author": "dimitrov-d",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.17.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"solana-agent-kit": "^1.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@typescript-eslint/parser": "8.19.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"eslint-plugin-sonarjs": "^3.0.1",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.4.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ext": "*.ts",
|
||||
"exec": "tsx",
|
||||
"delay": 1000
|
||||
}
|
||||
}
|
||||
8533
examples/discord-bot-starter/pnpm-lock.yaml
generated
Normal file
8533
examples/discord-bot-starter/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
96
examples/discord-bot-starter/src/index.ts
Normal file
96
examples/discord-bot-starter/src/index.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'dotenv/config';
|
||||
import { Client, GatewayIntentBits, Events, ChannelType, Partials } from 'discord.js';
|
||||
import { HumanMessage } from '@langchain/core/messages';
|
||||
import { MemorySaver } from '@langchain/langgraph';
|
||||
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { SolanaAgentKit, createSolanaTools } from 'solana-agent-kit';
|
||||
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages],
|
||||
partials: [Partials.Message, Partials.Channel],
|
||||
});
|
||||
|
||||
const chatHistory = new Map();
|
||||
|
||||
async function initializeAgent() {
|
||||
try {
|
||||
const llm = new ChatOpenAI({
|
||||
modelName: 'gpt-4o-mini',
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const solanaAgent = new SolanaAgentKit(process.env.SOLANA_PRIVATE_KEY!, process.env.SOLANA_RPC_URL!, {
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY!,
|
||||
// other config options here
|
||||
});
|
||||
|
||||
const tools = createSolanaTools(solanaAgent);
|
||||
|
||||
const memory = new MemorySaver();
|
||||
const config = { configurable: { thread_id: 'Solana Agent Kit!' } };
|
||||
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools,
|
||||
checkpointSaver: memory,
|
||||
messageModifier: `
|
||||
You are a helpful agent that can interact onchain using the Solana Agent Kit. You are
|
||||
empowered to interact onchain using your tools. If you ever need funds, you can request them from the
|
||||
faucet. If not, you can provide your wallet details and request funds from the user. If there is a 5XX
|
||||
(internal) HTTP error code, ask the user to try again later. If someone asks you to do something you
|
||||
can't do with your currently available tools, you must say so, and encourage them to implement it
|
||||
themselves using the Solana Agent Kit, recommend they go to https://www.solanaagentkit.xyz for more information. Be
|
||||
concise and helpful with your responses. Refrain from restating your tools' descriptions unless it is explicitly requested.
|
||||
`,
|
||||
});
|
||||
|
||||
return { agent, config };
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize agent:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
client.on(Events.ClientReady, async () => {
|
||||
// gets data about the bot
|
||||
await client.application?.fetch();
|
||||
|
||||
console.info(`${client.user?.username || 'Bot'} is running. Send it a message in Discord DM to get started.`);
|
||||
});
|
||||
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
try {
|
||||
if (message.channel.type !== ChannelType.DM || message.author.bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`Received message: ${message.content}`);
|
||||
await message.channel.sendTyping();
|
||||
|
||||
const { agent, config } = await initializeAgent();
|
||||
|
||||
const userId = message.author.id;
|
||||
if (!chatHistory.has(userId)) {
|
||||
chatHistory.set(userId, []);
|
||||
}
|
||||
const userChatHistory = chatHistory.get(userId);
|
||||
userChatHistory.push(new HumanMessage(message.content));
|
||||
|
||||
const stream = await agent.stream({ messages: userChatHistory }, config);
|
||||
|
||||
const replyIfNotEmpty = async (content: string) => content.trim() !== '' && message.reply(content);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if ('agent' in chunk) {
|
||||
const agentMessage = chunk.agent.messages[0].content;
|
||||
await replyIfNotEmpty(agentMessage);
|
||||
userChatHistory.push(new HumanMessage(agentMessage));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
client.login(process.env.DISCORD_BOT_TOKEN);
|
||||
21
examples/discord-bot-starter/tsconfig.json
Normal file
21
examples/discord-bot-starter/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2020",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2018", "dom", "esnext.asynciterable"],
|
||||
"plugins": [{ "transform": "typescript-transform-paths" }],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user