Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9db1d05
Add basic art GPT scaffolding
jakerockland Apr 27, 2023
8214044
Basic queries
jakerockland Apr 27, 2023
a648ff3
add langchain
jakerockland Apr 27, 2023
8ad2c64
Move artgpt to a class
jakerockland Apr 27, 2023
ec53ee5
lofi https://www.youtube.com/watch\?v\=jfKfPfyJRdk
jakerockland Apr 27, 2023
8a7b2b2
ARTBOT GPT
jakerockland Apr 27, 2023
0cd6f25
basedwagmiyolo
jakerockland Apr 27, 2023
c0dff4e
better format
jakerockland Apr 27, 2023
1ec1983
basic langchain integration (I think – lol)
jakerockland Apr 27, 2023
c6443fe
LANG CHAIN
jakerockland Apr 27, 2023
2c932ba
lil adjustments
jakerockland Apr 27, 2023
20fdd21
smol
jakerockland Apr 27, 2023
6224f38
minor
jakerockland Apr 27, 2023
b27258d
summarize if too long
jakerockland Apr 27, 2023
1022e4b
adding data ingestion prototype
lyaunzbe Apr 27, 2023
9375157
Merge branch 'artGPT' of github.com:ArtBlocks/artbot into artGPT
lyaunzbe Apr 27, 2023
d81cf67
Cleanup
jakerockland Apr 27, 2023
11cea13
Replies
jakerockland Apr 27, 2023
4626cc8
Merge branch 'artGPT' of github.com:ArtBlocks/artbot into artGPT
jakerockland Apr 27, 2023
caec93d
minor
jakerockland Apr 27, 2023
da0235c
open AI settings
jakerockland Apr 27, 2023
ffa2d10
fix order
jakerockland Apr 27, 2023
bde0253
Mess with the params
jakerockland Apr 27, 2023
57119d0
Cleanup' .
jakerockland Apr 28, 2023
0c0526c
minor
jakerockland Apr 28, 2023
fef93eb
Make the embeddings more recursive
jakerockland Apr 28, 2023
8532508
improve docs
jakerockland Apr 28, 2023
86b6939
recurrsive fetch
jakerockland Apr 28, 2023
5106f55
try catch
jakerockland Apr 28, 2023
119cc67
handle md and sol
jakerockland Apr 28, 2023
95745b2
dataIngestor updates around recursive repo crawling
lyaunzbe Apr 28, 2023
5608348
merging + fixing merge conflicts
lyaunzbe Apr 28, 2023
4e5c7cb
comments
jakerockland Apr 28, 2023
2cb1a59
add channel validation
jakerockland Apr 28, 2023
6c64606
remove todo
jakerockland Apr 28, 2023
4d57f79
SMol adjusts
jakerockland Apr 28, 2023
5d18e55
minor adjustments
jakerockland Apr 28, 2023
0fffa5c
import chaos
jakerockland Apr 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,10 @@ RESERVOIR_API_KEY=<contact maintainer for artbot-jr if token is desired>
DISCORD_TOKEN=<contact maintainer for artbot-jr if token is desired>
ETHERSCAN_API_KEY=<contact maintainer for artbot-jr if token is desired>
MINT_REFRESH_TIME_SECONDS=30
PRODUCTION_MODE=true
PRODUCTION_MODE=true

PINECONE_API_KEY=
PINECONE_ENV=
PINECONE_INDEX_NAME=

OPENAI_API_KEY=
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# ArtBot: The Art Blocks Discord Bot

![Build status](https://github.com/ArtBlocks/artbot/actions/workflows/build-check.yml/badge.svg)
[![GitPOAPs](https://public-api.gitpoap.io/v1/repo/ArtBlocks/artbot/badge)](https://www.gitpoap.io/gh/ArtBlocks/artbot)

Expand Down Expand Up @@ -51,17 +52,27 @@ The core engine of Artbot is built around the discord.js package. It serves seve

- All projects and their metadata are retrieved from the subgraph on startup in the `ArtIndexerBot.ts` class, which in turn creates a `ProjectBot` for every project. `#[n] [project_name]`, `#?`, etc queries are triaged by the `ArtIndexerBot` class, and the corresponding `ProjectBot` is triggered to respond.
- Curated artist channels are handled a bit differently. ProjectBots for the artist's projects are defined in `ProjectConfig/channels.json` and are triggered by the artist's name in the query. e.g. `#1 ringer` in `#dmitri-cherniak` will trigger the Ringer project bot.
- Additional configuration for these projects can be defined in `ProjectConfig/projectBots.json`. See [Adding query support for a project](#adding-query-support-for-a-project) for more details.
- Additional configuration for these projects can be defined in `ProjectConfig/projectBots.json`. See [Adding query support for a project](#adding-query-support-for-a-project) for more details.

- Sales/Listing Feeds

Artbot also provides a feeds for sales and listings of Art Blocks projects. It polls the (incredible) [Reservoir API](https://docs.reservoir.tools/reference/overview) to get the latest activity across all marketplaces (using the `ReservoirListBot.ts` and `ReservoirSaleBot.ts` classes, respectively), and then posts them to the appropriate Discord channels (`Utils/activityTriager.js`).
Artbot also provides a feeds for sales and listings of Art Blocks projects. It polls the (incredible) [Reservoir API](https://docs.reservoir.tools/reference/overview) to get the latest activity across all marketplaces (using the `ReservoirListBot.ts` and `ReservoirSaleBot.ts` classes, respectively), and then posts them to the appropriate Discord channels (`Utils/activityTriager.ts`).

- SmartBot Responses

Artbot has been taught to respond to some specific queries about gas price, curated/playground/factory, etc. when directly queried. This logic lives in `Utils/smartBotResponse.js`.
Artbot has been taught to respond to some specific queries about gas price, curated/playground/factory, etc. when directly queried. This logic lives in `Utils/smartBotResponse.ts`.

- ArtBotGPT Responses

Artbot has been given the power of GPT-3.5 and given the data of our public docs and smart-contracts repos:

- https://github.com/ArtBlocks/artblocks-docs
- https://github.com/ArtBlocks/artblocks-contracts

This logic lives in `Classes/ArtGPTBot.ts` and is queried in Discord via `?artGPT` commands.

## Adding query support for a project

### Definitions

#### Bot ID
Expand Down Expand Up @@ -118,7 +129,6 @@ Here are the currently valid contract names.
- `NamedMappings/<projectName>Seets.json`
- json file defining trigger names for single tokens. See `ringerSets.json` for example.


## PBAB instructions

These instructions explain how to configure Art Bot to serve project data in relevant channels.
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "A bot to provide info for artblocks",
"main": "index.js",
"scripts": {
"postinstall": "cd ./node_modules/langchain && yarn install && yarn run build && cd ../..",
"start": "ts-node src/index.ts",
"test": "jest",
"format": "prettier --write .",
Expand All @@ -22,12 +23,13 @@
"license": "MIT",
"dependencies": {
"@graphql-tools/utils": "^8.3.0",
"@pinecone-database/pinecone": "^0.0.14",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"axios": "^1.1.3",
"body-parser": "^1.15.2",
"discord.js": "^14.6.0",
"dotenv": "^8.2.0",
"dotenv": "^16.0.3",
"eslint": "^8.26.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
Expand All @@ -37,6 +39,7 @@
"googleapis": "^92.0.0",
"graphql": "^16.6.0",
"jest": "^28.0.3",
"langchain": "https://github.com/ArtBlocks/langchainjs#OpenAIChat-token-estimation-fix",
"lodash.deburr": "^4.1.0",
"ms": "^2.0.0",
"node-fetch": "^2.6.1",
Expand Down
287 changes: 287 additions & 0 deletions src/Classes/ArtGPTBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { Message, EmbedBuilder } from 'discord.js'

import { PineconeClient } from '@pinecone-database/pinecone'
// NOTE: Update to `langchain` once https://github.com/hwchase17/langchainjs/pull/1043
// is merged and published to npm
import { VectorDBQAChain } from 'langchain/langchain/chains'
import { OpenAIEmbeddings } from 'langchain/langchain/embeddings/openai'
import { OpenAI } from 'langchain/langchain/llms/openai'
import { PineconeStore } from 'langchain/langchain/vectorstores/pinecone'
import { VectorOperationsApi } from '@pinecone-database/pinecone/dist/pinecone-generated-ts-fetch'

// LLM Environment Variables
const PINECONE_API_KEY = process.env.PINECONE_API_KEY
const PINECONE_ENV = process.env.PINECONE_ENV
const PINECONE_INDEX_NAME = process.env.PINECONE_INDEX_NAME
// NOTE: OPENAI_API_KEY is not needed to be imported directly,
// but it is assumed by Langchain to be available in `process.env`
// so must be present in the .`env` file.

// ArtBot username
const ARTBOT_USERNAME = 'artbot'
const ARTBOT_MAX_CHARS_RESPONSE = 4000

// Discord consts
const DISCORD_INC_SERVER_ID = '822311470133542912'
const DISCORD_COMMUNITY_SERVER_ID = '411959613370400778'
const DISCORD_TEST_SERVER_ID = '785144843986665472'
const DISCORD_COMMUNITY_ARTIST_TECH_CHANNEL_ID = '909525641622347806'
const DISCORD_COMMUNITY_PARTNERSHIP_ARTISTS_CHANNEL_ID = '971541479333965824'

// Color consts
const ARTBOT_GREEN = 0x00ff00
const ARTBOT_WARNING = 0xffff00
const ARTBOT_ERROR = 0xff0000

// Rate limit constants
const MAX_REQUESTS_PER_HOUR = 50
const HOUR_IN_MILLISECONDS = 3600000

/**
* Bot for handling GPT-3.5 powered requests.
*/
export class ArtGPTBot {
queryString = '?artgpt'
lastRequestTimestamp: number
currentRequestCount: number
isLangChainWarmedUp: boolean
model: OpenAI
pineconeClient: PineconeClient
pineconeIndex: VectorOperationsApi | undefined // Initialized async
vectorStore: PineconeStore | undefined // Initialized async
langChain: VectorDBQAChain | undefined // Initialized async

constructor() {
this.lastRequestTimestamp = Date.now()
this.currentRequestCount = 0
// expect this to be set to `true` within initializeLangchain()
this.isLangChainWarmedUp = false
this.model = new OpenAI({
modelName: 'gpt-3.5-turbo',
temperature: 0,
// TODO: figure why setting -1 breaks with current setup
// Manually setting this value is causing issues if vector store retrieves
// too many documents...
maxTokens: -1,
})
this.pineconeClient = new PineconeClient()
this.initializeLangchain()
}

/**
* Helper to initialize langchain setup.
*/
async initializeLangchain() {
// Validity check the environment variables
if (!PINECONE_API_KEY) {
console.error('PINECONE_API_KEY not found in environment variables.')
return
}
if (!PINECONE_ENV) {
console.error('PINECONE_ENV not found in environment variables.')
return
}
if (!PINECONE_INDEX_NAME) {
console.error('PINECONE_INDEX_NAME not found in environment variables.')
return
}

// Initialize langchain setup
await this.pineconeClient.init({
apiKey: PINECONE_API_KEY,
environment: PINECONE_ENV,
})
const pineconeIndex = this.pineconeClient.Index(PINECONE_INDEX_NAME)
this.pineconeIndex = pineconeIndex
this.vectorStore = await PineconeStore.fromExistingIndex(
new OpenAIEmbeddings(),
{ pineconeIndex }
)
this.langChain = VectorDBQAChain.fromLLM(this.model, this.vectorStore, {
k: 2, // This is the number of documents to include as context (4 is default).
// Can turn this on (and log `response.sourceDocuments`) for debuggings purposes.
returnSourceDocuments: false,
})

// We are now warmed up!
this.isLangChainWarmedUp = true
}

/**
* Helper to determine if the bot is currently rate-limited.
*/
isRateLimited(): boolean {
// Check if we're in a new hour
if (Date.now() - this.lastRequestTimestamp > HOUR_IN_MILLISECONDS) {
// If so, reset the request count
this.lastRequestTimestamp = Date.now()
this.currentRequestCount = 0
}

// Increment the request count
this.currentRequestCount++

// Check if we're over the request limit
return this.currentRequestCount >= MAX_REQUESTS_PER_HOUR
}

/**
* Helper to determine if the bot is being queries in valid server and channel.
*/
inValidServerChannel(msg: Message): boolean {
const serverID = msg.guild ? msg.guild.id : ''
const channelID = msg.channel ? msg.channel.id : ''

if (
serverID == DISCORD_INC_SERVER_ID ||
serverID == DISCORD_TEST_SERVER_ID
) {
// Handle all messages in the Inc and test servers
return true
} else if (serverID == DISCORD_COMMUNITY_SERVER_ID) {
// In the community server, only field reqeusts in the
// #artist-tech and #partnership-artists channels for now
if (
channelID == DISCORD_COMMUNITY_ARTIST_TECH_CHANNEL_ID ||
channelID == DISCORD_COMMUNITY_PARTNERSHIP_ARTISTS_CHANNEL_ID
) {
return true
}
}
return false
}

/**
* Send an embed reply to a message.
* @param msg The message to reply to.
*/
async handleRequest(msg: Message) {
/*
* NOTE: It is important to check if the message author is the ArtBot
* Itself to avoid a recursive infinite loop.
*/
if (msg.author.username == ARTBOT_USERNAME) {
return null
}

const content = msg.content
const query = content.substring(this.queryString.length + 1, content.length)
if (this.inValidServerChannel(msg) === false) {
// Validate server / channel
this.sendWarningReply(
msg,
"I'm sorry, I'm not currently available in this server / channel."
)
return
} else if (content.length <= this.queryString.length) {
// Validate request format
this.sendWarningReply(
msg,
`Invalid format, enter ${this.queryString} followed by the query for ArtGPT.`
)
return
} else if (!this.isLangChainWarmedUp || !this.langChain) {
// Validate warm-up
const message = `
I'm sorry, I'm still warming up.

Please try again in a few minutes.
`
this.sendWarningReply(msg, message)
return
} else if (this.isRateLimited() === true) {
// Validate rate-limit
const message = `
I'm sorry, I'm rate-limited right now.

I currently can only process ${MAX_REQUESTS_PER_HOUR} requests per hour.

Please try again later.
`
this.sendWarningReply(msg, message)
return
} else {
// Give a "I'm thinking response" while we wait for the response.
this.sendEmbedReply(
msg,
ARTBOT_GREEN,
"Your question has been recieved! I'm working on an answer..."
)

// Query the langchain
let response
try {
response = await this.langChain.call({ query: query })
} catch (error) {
console.error(`Error calling langchain: ${error}`)
// TODO: Remove extra logging once we're confident in the bot.
console.log(error.response?.data)
console.log(error)
this.sendErrorReply(msg)
return
}

// Summarize response to be less than ARTBOT_MAX_CHARS_RESPONSE if it is too long.
if (response.text.length > ARTBOT_MAX_CHARS_RESPONSE) {
console.log('Summarizing response...')
try {
response = await this.langChain.call({
query: `
Please summarize the following response to be less than ${ARTBOT_MAX_CHARS_RESPONSE} characters:
---
${query}
`,
})
} catch (error) {
console.error(`Error summarizing with langchain: ${error}`)
this.sendErrorReply(msg)
return
}
}

// Provide the real response.
const message = `
*NOTE: I am still in beta, my answers may be wrong.*

${response.text}
`
this.sendEmbedReply(msg, ARTBOT_GREEN, message)
}
}

/**
* Send an embed reply to a message.
* @param msg The message to reply to.
* @param title The title of the embed.
* @param color The color of the embed.
* @param description The description of the embed.
*/
async sendEmbedReply(msg: Message, color: number, description: string) {
const embed = new EmbedBuilder()
.setTitle(this.queryString)
.setColor(color)
.setDescription(description)

await msg.reply({ embeds: [embed] })
}

/**
* Send an warning reply to a message.
* @param msg The message to reply to.
*/
async sendWarningReply(msg: Message, warning: string) {
this.sendEmbedReply(msg, ARTBOT_WARNING, warning)
}

/**
* Send an error reply to a message.
* @param msg The message to reply to.
*/
async sendErrorReply(msg: Message) {
this.sendEmbedReply(
msg,
ARTBOT_ERROR,
"I'm sorry, I encountered an error. Please try again later."
)
}
}
Loading