Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] ArtGPT! (v0) #503

Draft
wants to merge 55 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
55 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
2464067
Better system context
jakerockland Apr 28, 2023
32fc1fe
monkeypatch while waiting for https://github.com/hwchase17/langchainj…
jakerockland Apr 29, 2023
f322630
monkeypatch while waiting for https://github.com/hwchase17/langchainj…
jakerockland Apr 29, 2023
666f220
add metadata tagging to pinecone embeds
lyaunzbe Apr 29, 2023
497dd78
Merge branch 'artGPT' of github.com:ArtBlocks/artbot into artGPT
lyaunzbe Apr 29, 2023
9f962f2
Update to pass 3 docs, and include better prompt
jakerockland Apr 29, 2023
eb48ff8
Merge branch 'artGPT' of github.com:ArtBlocks/artbot into artGPT
jakerockland Apr 29, 2023
caae6e9
adjust the params
jakerockland Apr 29, 2023
613a058
better outputs, and remove unnecessary maxTokens logic
jakerockland May 1, 2023
5d1e5f6
better prompt
jakerockland May 1, 2023
45c7d14
Minor tweak
jakerockland May 2, 2023
7ebaa4c
Merge branch 'main' into artGPT
jakerockland May 2, 2023
dba6bf4
update initial server permissions
jakerockland May 2, 2023
e17a1bd
Add channel config for Artbot in Art Blocks Inc server
jakerockland May 2, 2023
133721a
Fix equality checks
jakerockland May 2, 2023
639aeb9
Fix equality checks
jakerockland May 2, 2023
b10bd03
clarifying comment
jakerockland May 2, 2023
2ef73fb
updated env example
jakerockland May 2, 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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,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 +38,7 @@
"googleapis": "^92.0.0",
"graphql": "^16.6.0",
"jest": "^28.0.3",
"langchain": "^0.0.66",
"lodash.deburr": "^4.1.0",
"ms": "^2.0.0",
"node-fetch": "^2.6.1",
Expand Down
306 changes: 306 additions & 0 deletions src/Classes/ArtGPTBot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
import { Message, EmbedBuilder } from 'discord.js'

import { PineconeClient } from '@pinecone-database/pinecone'
import { VectorDBQAChain } from 'langchain/chains'
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
import { OpenAIChat } from 'langchain/llms/openai'
import { PineconeStore } from '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_TEST_SERVER_ID = '785144843986665472'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this will be included in the remaining config changes you mentioned, but these should be pulled from the channels.json - perhaps we can add an optional field for channels where we want this enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a good point – we definitely could add this information to be exposed via projectConfig.ts rather than being inline directly, is that your suggestion/preference @grantoesterling ? I don't have a strong opinion on this.

const DISCORD_INC_SERVER_ID = '822311470133542912'
const DISCORD_INC_ARTGPT_CHANNEL_ID = '1103051072756519012'

// 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: OpenAIChat
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 OpenAIChat({
modelName: 'gpt-3.5-turbo', // With valid API keys can also use 'gpt-4'
temperature: 0,
prefixMessages: [
{
role: 'system',
content: `
You are an software integration and project support assistant for
Art Blocks artists and Art Blocks Engine integration partners.
You have been trained on github repositories containing the Art Blocks
Solidity smart contracts and the documentation that covers: these smart
contracts, the Art Blocks APIs (for token metadata, live rendering, etc.),
and the processes for using these APIs, contracts, and tools.
`,
},
],
})
this.pineconeClient = new PineconeClient()
this.initializeLangchain()
}

/**
* Helper to initialize langchain setup.
*/
async initializeLangchain() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm i think an essential part of this is that people should be able to run/develop artbot still without needing to have these new GPT env vars set. Looking at the deployment, it seems langchain is throwing an error due to no OpenAI API key and crashing the whole program

Can you please make sure this fails gracefully when no env vars are set?

// 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).
returnSourceDocuments: true,
})

// 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 : ''

// For now, we only handle messages in the Inc and test servers
if (serverID == DISCORD_TEST_SERVER_ID) {
jakerockland marked this conversation as resolved.
Show resolved Hide resolved
// Handle all messages in the test server
return true
} else if (serverID == DISCORD_INC_SERVER_ID) {
// Only handle messages in the specified #artgpt channel
if (channelID == DISCORD_INC_ARTGPT_CHANNEL_ID) {
jakerockland marked this conversation as resolved.
Show resolved Hide resolved
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) {
jakerockland marked this conversation as resolved.
Show resolved Hide resolved
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: ${JSON.stringify(error)}`)
console.error(
`Error response data: ${JSON.stringify(error.response?.data)}`
)
this.sendErrorReply(msg)
return
}
const sourceDocuments = response.sourceDocuments
const sourceLocations = sourceDocuments.map((doc: any) => ({
repoName: doc.metadata.repoName,
fileName: doc.metadata.fileName,
}))
let sourceLocationsString = ''
sourceLocations.forEach((location: any) => {
sourceLocationsString += `
- ${location.fileName} in ${location.repoName}
`
})

// Summarize response to be less than ARTBOT_MAX_CHARS_RESPONSE if it is too long.
if (
response.text.length + sourceLocationsString.length >
ARTBOT_MAX_CHARS_RESPONSE
grantoesterling marked this conversation as resolved.
Show resolved Hide resolved
) {
console.log('Summarizing response...')
try {
response = await this.langChain.call({
query: `
Please summarize the following response to be less than ${
ARTBOT_MAX_CHARS_RESPONSE - sourceLocationsString.length
} 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}

---

*Source Documents:*
${sourceLocationsString}
`
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