Compare commits

...

6 Commits

Author SHA1 Message Date
Kevin Dang
43fb2ea94e User Preferences and Setup Docs (#20)
* added message style command

* docker setup scripts

* reformat messageStyle.ts

* fix: register unregister on deploy

* add: messageStream preference

* add: json config handler

* update: messageCreate gets config

* update: shifted chat to config callback

* fix: naming conventions based on discord

* update: setup in docs now

* add: static docker ips

* version increment

* add: bot message for no config

* fix: no config case

* add: clarification for subnetting

* update: version increment in lock file

---------

Co-authored-by: JT2M0L3Y <jtsmoley@icloud.com>
2024-03-22 10:37:06 -07:00
Kevin Dang
5e74736c57 Small Documentation and Refactoring (#18)
* cleanup and documentation

* added dev message for parser

* grammar and other type replacements
2024-02-18 17:39:00 -08:00
Kevin Dang
1c62958c9f docker setup instructions 2024-02-07 11:41:15 -08:00
Kevin Dang
ca6b8c3f9c Docker Container Setup (#15)
* minor package update and env

* added docker scripts

* added working docker compose

* fixed docker container bridge
2024-02-07 09:59:06 -08:00
Kevin Dang
89c19990fa slash commands integrated
* sample env and late version incr

* added slash command compatibility

* updated command name

* updated environment sample

* updated interaction comment
2024-01-31 10:28:02 -08:00
Kevin Dang
b94ff55449 formatting and contributing
* fixed some formatting

* contributing format

* simple style rules
2024-01-30 16:15:35 -08:00
30 changed files with 779 additions and 559 deletions

24
.env.sample Normal file
View File

@@ -0,0 +1,24 @@
# Discord token for the bot
CLIENT_TOKEN = BOT_TOKEN
# id token of a discord server
GUILD_ID = GUILD_ID
# Channel where the bot listens to messages
CHANNEL_ID = CHANNEL_ID
# model for the bot to query from (i.e. llama2 [llama2:13b], mistral, codellama, etc... )
MODEL = MODEL_NAME
# discord bot user id for mentions
CLIENT_UID = BOT_USER_ID
# ip/port address of docker container, I use 172.18.X.X for docker, 127.0.0.1 for local
OLLAMA_IP = IP_ADDRESS
OLLAMA_PORT = PORT
# ip address for discord bot container, I use 172.18.X.X, use different IP than ollama_ip
DISCORD_IP = IP_ADDRESS
# subnet address, ex. 172.18.0.0 as we use /16.
SUBNET_ADDRESS = ADDRESS

38
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,38 @@
<!--
Author: Kevin Dang
Date: 1-30-2024
-->
## Run the Bot
* Refer to all sections below before running the bot.
* You should now have `Ollama`, `NodeJS`, ran `npm install`.
* You will also need a discord bot to run. Refer to the [developer portal](https://discord.com/developers/) to learn how to set one up and invite it to your server. If that does not help then look up a YouTube video like this [one](https://www.youtube.com/watch?v=KZ3tIGHU314&ab_channel=UnderCtrl).
* Now run `npm run start` to run the client and ollama at the same time (this must be one in wsl or a Linux distro)
## Set up (Development-side)
* Pull the repository using `https://github.com/kevinthedang/discord-ollama.git`.
* Refer to `Ollama Setup` in the readme to set up Ollama.
* This must be set up in a Linux environment or wsl2.
* Install NodeJS `v18.18.2`
* You can check out `Resources` and `To Run` in the readme for a bit of help.
* You can also reference [NodeJS Setup](#nodejs-setup)
* When you have the project pulled from github, open up a terminal and run `npm i` or `npm install` to get all of the packages for the project.
* In some kind of terminal (`git bash` is good) to run the client. You can run Ollama but opening up wsl2 and typing `ollama serve`.
* Refer to `Ollama Setup` if there are any issues.
## Environment
* You will need two environment files:
* `.env`: for running the bot
* `CLIENT_TOKEN`: the token for the bot to log in
* `CHANNEL_ID`: the id of the channel you wish for the bot to listen in
* `MODEL`: the mode you wish to use
* `BOT_UID`: the user id the bot goes by (the id of the discord user)
* `.env.dev.local`: also runs the bot, but with development variables
* Currently there are no differences between the two, but when needed, you may add environment variables as needed.
## NodeJS Setup
* Install [nvm](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating) using `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash`
* Ensure this in the profile of what shell you use (for `git bash` it would be `.bash_profile` found in your home directory)
* Ensure it has been install correctly by running `nvm -v`
* Now, install `v18.18.2` by running `nvm install 18.18.2`
* Then run `nvm use 18.18.2 | nvm alias default 18.18.2` or you can run them separately if that does not work. This just sets the default NodeJS to `v18.18.2` when launching a shell.

5
.github/style.md vendored Normal file
View File

@@ -0,0 +1,5 @@
## Style Preferences
* Please just make sure that you are using a `Tab Default` of 4 for spacing
* You don't need semicolons at the end of everything.
* Comments for functions would be nice to help explain what they do and what the parameters are for.
* If there are any other issues, just refer to the [Google Style Guide](https://google.github.io/styleguide/tsguide.html)

10
.gitignore vendored
View File

@@ -1,6 +1,16 @@
# Created by https://www.toptal.com/developers/gitignore/api/node # Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node # Edit at https://www.toptal.com/developers/gitignore?templates=node
# config
config.json
# builds
build/
dist/
app/
tmp/
data/
# dotenv environment variable files # dotenv environment variable files
.env .env
.env.dev.local .env.dev.local

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
# use node LTS image for version 18
FROM node:18.18.2
# set working directory inside container
WORKDIR /app
# copy package.json and the lock file into the container, and src files
COPY ./src ./src
COPY ./*.json ./
COPY ./.env ./
# install dependencies, breaks
RUN npm install
# build the typescript code
RUN npm run build
# start the application
CMD ["npm", "run", "prod"]

View File

@@ -1,34 +1,28 @@
# Discord Ollama Integration [![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC_BY--NC_4.0-darkgreen.svg)](https://creativecommons.org/licenses/by-nc/4.0/) [![Release Badge](https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github)](https://github.com/kevinthedang/discord-ollama/releases/latest) # Discord Ollama Integration [![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC_BY--NC_4.0-darkgreen.svg)](https://creativecommons.org/licenses/by-nc/4.0/) [![Release Badge](https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github)](https://github.com/kevinthedang/discord-ollama/releases/latest)
Ollama is an AI model management tool that allows users to install and use custom large language models locally. The goal is to create a discord bot that will utilize Ollama and chat with it on a Discord! Ollama is an AI model management tool that allows users to install and use custom large language models locally. The goal is to create a discord bot that will utilize Ollama and chat with it on a Discord!
## Ollama Setup ## Environment Setup
* Go to Ollama's [Linux download page](https://ollama.ai/download/linux) and run the simple curl command they provide. The command should be `curl https://ollama.ai/install.sh | sh`.
* Now the the following commands in separate terminals to test out how it works!
* In terminal 1 -> `ollama serve` to setup ollama
* In terminal 2 -> `ollama run [model name]`, for example `ollama run llama2`
* The models can vary as you can create your own model. You can also view ollama's [library](https://ollama.ai/library) of models.
* This can also be done in [wsl](https://learn.microsoft.com/en-us/windows/wsl/install) for Windows machines.
* You can now interact with the model you just ran (it might take a second to startup).
* Response time varies with processing power!
## To Run
* Clone this repo using `git clone https://github.com/kevinthedang/discord-ollama.git` or just use [GitHub Desktop](https://desktop.github.com/) to clone the repo. * Clone this repo using `git clone https://github.com/kevinthedang/discord-ollama.git` or just use [GitHub Desktop](https://desktop.github.com/) to clone the repo.
* Run `npm install` to install the npm packages. * You will need a `.env` file in the root of the project directory with the bot's token. There is a `.env.sample` is provided for you as a reference for what environment variables.
* You will need a `.env` file in the root of the project directory with the bot's token.
* For example, `CLIENT_TOKEN = [Bot Token]` * For example, `CLIENT_TOKEN = [Bot Token]`
* Now, you can run the bot by running `npm run start` which will build and run the decompiled typescript and run the setup for ollama. * Please refer to the docs for bot setup. **NOTE**: These guides assume you already know how to setup a bot account for discord.
* **IMPORTANT**: This must be ran in the wsl/Linux instance to work properly! Using Command Prompt/Powershell/Git Bash/etc. will not work on Windows (at least in my experience). * [Local Machine Setup](./docs/setup-local.md)
* Refer to the [resources](#resources) on what node version to use. * [Docker Setup for Servers and Local Machines](./docs/setup-docker.md)
* Local use is not recommended.
## Resources ## Resources
* [NodeJS](https://nodejs.org/en) * [NodeJS](https://nodejs.org/en)
* This project uses `v20.10.0` (npm `10.2.5`). Consider using [nvm](https://github.com/nvm-sh/nvm) for multiple NodeJS versions. * This project uses `v20.10.0+` (npm `10.2.5`). Consider using [nvm](https://github.com/nvm-sh/nvm) for multiple NodeJS versions.
* To run dev in `ts-node`, using `v18.18.2` is recommended. **CAUTION**: `v18.19.0` or `lts/hydrogen` will not run properly. * To run dev in `ts-node`, using `v18.18.2` is recommended. **CAUTION**: `v18.19.0` or `lts/hydrogen` will not run properly.
* To run dev with `tsx`, you can use `v20.10.0` or earlier. * To run dev with `tsx`, you can use `v20.10.0` or earlier.
* This project supports any NodeJS version above `16.x.x` to only allow ESModules. * This project supports any NodeJS version above `16.x.x` to only allow ESModules.
* [Ollama](https://ollama.ai/) * [Ollama](https://ollama.ai/)
* [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama)
* **IMPORTANT**: For Nvidia GPU setup, **install** `nvidia container toolkit/runtime` then **configure** it with Docker to utilize Nvidia driver.
* [Discord Developer Portal](https://discord.com/developers/docs/intro) * [Discord Developer Portal](https://discord.com/developers/docs/intro)
* [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main) * [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main)
* [Setting up Docker (Ubuntu 20.04)](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
* [Setting up Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
## Acknowledgement ## Acknowledgement
* [Kevin Dang](https://github.com/kevinthedang) * [Kevin Dang](https://github.com/kevinthedang)

54
docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
# creates the docker compose
version: '3.7'
# build individual services
services:
# setup discord bot container
discord:
build: ./ # find docker file in designated path
container_name: discord
restart: always # rebuild container always
image: discord/bot:0.2.0
environment:
CLIENT_TOKEN: ${CLIENT_TOKEN}
GUILD_ID: ${GUILD_ID}
CHANNEL_ID: ${CHANNEL_ID}
MODEL: ${MODEL}
CLIENT_UID: ${CLIENT_UID}
OLLAMA_IP: ${OLLAMA_IP}
OLLAMA_PORT: ${OLLAMA_PORT}
networks:
ollama-net:
ipv4_address: ${DISCORD_IP}
volumes:
- discord:/src/app # docker will not make this for you, make it yourself
# setup ollama container
ollama:
image: ollama/ollama:latest # build the image using ollama
container_name: ollama
restart: always
networks:
ollama-net:
ipv4_address: ${OLLAMA_IP}
# runtime: nvidia # use Nvidia Container Toolkit for GPU support
# devices:
# - /dev/nvidia0
volumes:
- ollama:/root/.ollama
ports:
- ${OLLAMA_PORT}:${OLLAMA_PORT}
# create a network that supports giving addresses withing a specific subnet
networks:
ollama-net:
driver: bridge
ipam:
driver: default
config:
- subnet: ${SUBNET_ADDRESS}/16
volumes:
ollama:
discord:

37
docs/setup-docker.md Normal file
View File

@@ -0,0 +1,37 @@
## Docker Setup
* Follow this guide to setup [Docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
* If on Windows, download [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/) to get the docker engine.
* Please also install [Docker Compose](https://docs.docker.com/compose/install/linux/) for easy running. If not, there are [scripts](#manual-run-with-docker) to set everything up.
## To Run (with Docker and Docker Compose)
* With the inclusion of subnets in the `docker-compose.yml`, you will need to set the `SUBNET_ADDRESS`, `OLLAMA_IP`, `OLLAMA_PORT`, and `DISCORD_IP`. Here are some default values if you don't care:
* `OLLAMA_IP = 172.18.0.2`
* `OLLAMA_PORT = 11434`
* `DISCORD_IP = 172.18.0.3`
* `SUBNET_ADDRESS = 172.18.0.0`
* Don't understand any of this? watch a Networking video to understand subnetting.
* You will need a model in the container for this to work properly, on Docker Desktop go to the `Containers` tab, select the `ollama` container, and select `Exec` to run as root on your container. Now, run `ollama pull [model name]` to get your model.
* For Linux Servers, you need another shell to pull the model, or if you run `docker compose build && docker compose up -d`, then it will run in the background to keep your shell. Run `docker exec -it ollama bash` to get into the container and run the samme pull command above.
* Otherwise, there is no need to install any npm packages for this, you just need to run `npm run start` to pull the containers and spin them up.
* For cleaning up on Linux (or Windows), run the following commands:
* `docker compose stop`
* `docker compose rm`
* `docker ps` to check if containers have been removed.
* You can also use `npm run clean` to clean up the containers and remove the network to address a possible `Address already in use` problem.
## Manual Run (with Docker)
* Run the following commands:
* `npm run docker:build`
* `npm run docker:ollama`
* `npm run docker:client`
* `docker ps` to see if the containers are there!
* Names should be **discord** and **ollama**.
* You can also just run `npm run docker:start` now for the above commands.
* Clean-up:
* `docker ps` for the conatiner id's. Use `-a` flag as necessary.
* `docker rm -f discord && docker rm -f ollama` to remove the containers.
* `docker rm -f CONTAINER_ID` do for both containers if naming issues arise.
* `docker network rm ollama-net` removes the network.
* `docker network prune` will also work so long as the network is unused.
* Remove Image:
* If you need to remove the image run `docker image rm IMAGE_ID`. You can get the image id by running `docker images`.

19
docs/setup-local.md Normal file
View File

@@ -0,0 +1,19 @@
## Ollama Setup
* Go to Ollama's [Linux download page](https://ollama.ai/download/linux) and run the simple curl command they provide. The command should be `curl https://ollama.ai/install.sh | sh`.
* Now the the following commands in separate terminals to test out how it works!
* In terminal 1 -> `ollama serve` to setup ollama
* In terminal 2 -> `ollama run [model name]`, for example `ollama run llama2`
* The models can vary as you can create your own model. You can also view ollama's [library](https://ollama.ai/library) of models.
* If there are any issues running ollama because of missing LLMs, run `ollama pull [model name]` as it will pull the model if Ollama has it in their library.
* This can also be done in [wsl](https://learn.microsoft.com/en-us/windows/wsl/install) for Windows machines.
* You can now interact with the model you just ran (it might take a second to startup).
* Response time varies with processing power!
## To Run Locally (without Docker)
* Run `npm install` to install the npm packages.
* Ensure that your [.env](../.env.sample) file's `OLLAMA_IP` is `127.0.0.1` to work properly.
* Now, you can run the bot by running `npm run client` which will build and run the decompiled typescript and run the setup for ollama.
* **IMPORTANT**: This must be ran in the wsl/Linux instance to work properly! Using Command Prompt/Powershell/Git Bash/etc. will not work on Windows (at least in my experience).
* Refer to the [resources](../README.md#resources) on what node version to use.
* Open up a separate terminal/shell (you will need wsl for this if on windows) and run `ollama serve` to startup ollama.
* If you do not have a model, you will need to run `ollama pull [model name]` in a separate terminal to get it.

647
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,31 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.1.2", "version": "0.3.0",
"description": "Ollama Integration into discord", "description": "Ollama Integration into discord",
"main": "dist/index.js", "main": "build/index.js",
"exports": "./dist/index.js", "exports": "./build/index.js",
"scripts": { "scripts": {
"dev-tsx": "tsx watch src/index.ts", "dev-tsx": "tsx watch src/index.ts",
"dev-mon": "nodemon --config nodemon.json src/index.ts", "dev-mon": "nodemon --config nodemon.json src/index.ts",
"build": "tsc", "build": "tsc",
"prod": "node .", "prod": "node .",
"client": "npm run build && npm run prod", "client": "npm run build && npm run prod",
"API": "ollama serve", "clean": "docker compose down && docker rmi $(docker images | grep 0.2.0 | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"start": "concurrently \"npm:API\" \"npm:client\"" "start": "docker compose build --no-cache && docker compose up -d",
"docker:start": "npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama",
"docker:clean": "docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net",
"docker:build": "docker build --no-cache -t discord/bot:0.2.0 .",
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 discord",
"docker:ollama": "docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest"
}, },
"author": "Kevin Dang", "author": "Kevin Dang",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.6.2",
"concurrently": "^8.2.2",
"discord.js": "^14.14.1", "discord.js": "^14.14.1",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"ollama": "^0.4.3" "ollama": "^0.4.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.5", "@types/node": "^20.10.5",

View File

@@ -1,9 +1,11 @@
import { Client, GatewayIntentBits } from "discord.js"; import { Client, GatewayIntentBits } from 'discord.js'
import { registerEvents } from "./utils/events.js"; import { UserMessage, registerEvents } from './utils/events.js'
import Events from "./events/index.js"; import Events from './events/index.js'
import { Ollama } from 'ollama'
// Import keys/tokens // Import keys/tokens
import Keys from "./keys.js"; import Keys from './keys.js'
// initialize the client with the following permissions when logging in // initialize the client with the following permissions when logging in
const client = new Client({ const client = new Client({
@@ -15,7 +17,13 @@ const client = new Client({
] ]
}); });
const messageHistory = [ // initialize connection to ollama container
const ollama = new Ollama({
host: `http://${Keys.ipAddress}:${Keys.portAddress}`,
})
// Create Queue managed by Events
const messageHistory: [UserMessage] = [
{ {
role: 'system', role: 'system',
content: 'Your name is Ollama GU' content: 'Your name is Ollama GU'
@@ -29,11 +37,11 @@ const messageHistory = [
* @param client the bot reference * @param client the bot reference
* @param Keys tokens from .env files * @param Keys tokens from .env files
*/ */
registerEvents(client, Events, messageHistory, Keys) registerEvents(client, Events, messageHistory, Keys, ollama)
// Try to log in the client // Try to log in the client
client.login(Keys.clientToken) await client.login(Keys.clientToken)
.catch((error) => { .catch((error) => {
console.error('[Login Error]', error); console.error('[Login Error]', error)
process.exit(1); process.exit(1)
}); })

10
src/commands/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import { SlashCommand } from '../utils/commands.js'
import { ThreadCreate } from './threadCreate.js'
import { MessageStyle } from './messageStyle.js'
import { MessageStream } from './messageStream.js'
export default [
ThreadCreate,
MessageStyle,
MessageStream
] as SlashCommand[]

View File

@@ -0,0 +1,33 @@
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openFile } from '../utils/jsonHandler.js'
export const MessageStream: SlashCommand = {
name: 'message-stream',
description: 'change preference on message streaming from ollama. WARNING: can be very slow.',
// user option(s) for setting stream
options: [
{
name: 'stream',
description: 'enable or disable stream preference',
type: ApplicationCommandOptionType.Boolean,
required: true
}
],
// change preferences based on command
run: async (client: Client, interaction: CommandInteraction) => {
// verify channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
// save value to json and write to it
openFile('config.json', interaction.commandName, interaction.options.get('stream')?.value)
interaction.reply({
content: `Message streaming preferences for embed set to: \`${interaction.options.get('stream')?.value}\``,
ephemeral: true
})
}
}

View File

@@ -0,0 +1,33 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { openFile } from '../utils/jsonHandler.js'
export const MessageStyle: SlashCommand = {
name: 'message-style',
description: 'sets the message style to embed or normal',
// set available user options to pass to the command
options: [
{
name: 'embed',
description: 'toggle embedded or normal message',
type: ApplicationCommandOptionType.Boolean,
required: true
}
],
// Query for message information and set the style
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
// set the message style
openFile('config.json', interaction.commandName, interaction.options.get('embed')?.value)
interaction.reply({
content: `Message style preferences for embed set to: \`${interaction.options.get('embed')?.value}\``,
ephemeral: true
})
}
}

View File

@@ -0,0 +1,28 @@
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
export const ThreadCreate: SlashCommand = {
name: 'thread',
description: 'creates a thread and mentions user',
// Query for server information
run: async (client: Client, interaction: CommandInteraction) => {
// fetch the channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
const thread = await (channel as TextChannel).threads.create({
name: `support-${Date.now()}`,
reason: `Support ticket ${Date.now()}`
})
// Send a message in the thread
thread.send(`**User:** ${interaction.user} \n**People in Coversation:** ${thread.memberCount}`)
// user only reply
return interaction.reply({
content: `I can help you in the Thread below. \n**Thread ID:** ${thread.id}`,
ephemeral: true
})
}
}

View File

@@ -1,9 +1,11 @@
import { Event } from '../utils/index.js' import { Event } from '../utils/index.js'
import interactionCreate from './interactionCreate.js'
import messageCreate from './messageCreate.js' import messageCreate from './messageCreate.js'
import ready from './ready.js' import ready from './ready.js'
// Centralized export for all events // Centralized export for all events
export default [ export default [
ready, ready,
messageCreate messageCreate,
interactionCreate
] as Event[] // staticly is better ts practice, dynamic exporting is possible ] as Event[] // staticly is better ts practice, dynamic exporting is possible

View File

@@ -0,0 +1,19 @@
import { event, Events } from '../utils/index.js'
import commands from '../commands/index.js'
/**
* Interaction creation listener for the client
* @param interaction the interaction received from the server
*/
export default event(Events.InteractionCreate, async ({ log, client }, interaction) => {
if (!interaction.isCommand() || !interaction.isChatInputCommand()) return
log(`Interaction called \'${interaction.commandName}\' from ${interaction.user.tag}.`)
// ensure command exists, otherwise kill event
const command = commands.find(command => command.name === interaction.commandName)
if (!command) return
// the command exists, execute it
command.run(client, interaction)
})

View File

@@ -1,11 +1,13 @@
import { embedMessage, event, Events } from '../utils/index.js' import { ChatResponse } from 'ollama'
import { embedMessage, event, Events, normalMessage } from '../utils/index.js'
import { Configuration, getConfig } from '../utils/jsonHandler.js'
/** /**
* Max Message length for free users is 2000 characters (bot or not). * Max Message length for free users is 2000 characters (bot or not).
* @param message the message received from the channel * @param message the message received from the channel
*/ */
export default event(Events.MessageCreate, async ({ log, msgHist, tokens }, message) => { export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama }, message) => {
log(`Message created \"${message.content}\" from ${message.author.tag}.`) log(`Message \"${message.content}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
// Hard-coded channel to test output there only, in our case "ollama-endpoint" // Hard-coded channel to test output there only, in our case "ollama-endpoint"
if (message.channelId != tokens.channel) return if (message.channelId != tokens.channel) return
@@ -14,7 +16,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens }, mess
if (message.author.tag === message.client.user.tag) return if (message.author.tag === message.client.user.tag) return
// Only respond if message mentions the bot // Only respond if message mentions the bot
if (!message.mentions.has(tokens.botUid)) return if (!message.mentions.has(tokens.clientUid)) return
// push user response // push user response
msgHist.push({ msgHist.push({
@@ -22,18 +24,36 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens }, mess
content: message.content content: message.content
}) })
// Try to query and send embed // Try to query and send embed
const response = await embedMessage(message, tokens, msgHist) try {
const config: Configuration = await new Promise((resolve, reject) => {
getConfig('config.json', (config) => {
if (config === undefined) {
reject(new Error('No Configuration is set up.'))
return
}
resolve(config)
})
})
// Try to query and send message let response: ChatResponse
// log(normalMessage(message, tokens, msgHist))
// If something bad happened, remove user query and stop // undefined or false, use normal, otherwise use embed
if (response == undefined) { msgHist.pop(); return } if (config.options['message-style'])
response = await embedMessage(message, ollama, tokens, msgHist)
else
response = await normalMessage(message, ollama, tokens, msgHist)
// successful query, save it as history // If something bad happened, remove user query and stop
msgHist.push({ if (response == undefined) { msgHist.pop(); return }
role: 'assistant',
content: response.message.content // successful query, save it as history
}) msgHist.push({
role: 'assistant',
content: response.message.content
})
} catch (error: any) {
msgHist.pop() // remove message because of failure
message.reply(`**Response generation failed.**\n\nReason: ${error.message}\n\nPlease use any config slash command.`)
}
}) })

View File

@@ -1,6 +1,17 @@
import { event, Events } from '../utils/index.js' import { event, Events, registerCommands } from '../utils/index.js'
import { ActivityType } from 'discord.js'
import commands from '../commands/index.js'
// Log when the bot successfully logs in and export it // Log when the bot successfully logs in and export it
export default event(Events.ClientReady, ({ log }, client) => { export default event(Events.ClientReady, ({ log }, client) => {
return log(`Logged in as ${client.user.username}.`) // Register the commands associated with the bot upon loggin in
registerCommands(client, commands)
// set status of the bot
client.user.setActivity({
name: 'Powered by Ollama',
type: ActivityType.Custom
})
log(`Logged in as ${client.user.username}.`)
}) })

View File

@@ -1,10 +1,13 @@
import { getEnvVar } from "./utils/env.js" import { getEnvVar } from './utils/env.js'
export const Keys = { export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'), clientToken: getEnvVar('CLIENT_TOKEN'),
channel: getEnvVar('CHANNEL_ID'), channel: getEnvVar('CHANNEL_ID'),
model: getEnvVar('MODEL'), model: getEnvVar('MODEL'),
botUid: getEnvVar('BOT_UID') clientUid: getEnvVar('CLIENT_UID'),
guildId: getEnvVar('GUILD_ID'),
ipAddress: getEnvVar('OLLAMA_IP'),
portAddress: getEnvVar('OLLAMA_PORT')
} as const // readonly keys } as const // readonly keys
export default Keys export default Keys

47
src/utils/commands.ts Normal file
View File

@@ -0,0 +1,47 @@
import { CommandInteraction, ChatInputApplicationCommandData, Client, ApplicationCommandOption } from 'discord.js'
/**
* interface for how slash commands should be run
*/
export interface SlashCommand extends ChatInputApplicationCommandData {
run: (
client: Client,
interaction: CommandInteraction,
options?: ApplicationCommandOption[]
) => void
}
/**
* register the command to discord for the channel
* @param client the bot reference
* @param commands commands to register to the bot
*/
export function registerCommands(client: Client, commands: SlashCommand[]): void {
// ensure the bot is online before registering
if (!client.application) return
// map commands into an array of names, used to checking registered commands
const commandsToRegister: string[] = commands.map(command => command.name)
// fetch all the commands and delete them
client.application.commands.fetch().then((fetchedCommands) => {
for (const command of fetchedCommands.values()) {
if (!commandsToRegister.includes(command.name)) {
command.delete().catch(console.error)
console.log(`[Command: ${command.name}] Removed from Discord`)
}
}
})
// clear the cache of the commands
client.application.commands.cache.clear()
// iterate through all commands and register them with the bot
for (const command of commands)
client.application.commands
.create(command)
.then((c) => {
console.log(`[Command: ${c.name}] Registered on Discord`)
c.options?.forEach((o) => console.log(` - ${o.name}`))
})
}

View File

@@ -1,8 +1,8 @@
import { resolve } from "path" import { resolve } from 'path'
import { config } from "dotenv" import { config } from 'dotenv'
// Find config - ONLY WORKS WITH NODEMON // Find config - ONLY WORKS WITH NODEMON
const envFile = process.env.NODE_ENV === "development" ? ".env.dev.local" : ".env" const envFile = process.env.NODE_ENV === 'development' ? '.env.dev.local' : '.env'
// resolve config file // resolve config file
const envFilePath = resolve(process.cwd(), envFile) const envFilePath = resolve(process.cwd(), envFile)

View File

@@ -1,4 +1,5 @@
import type { ClientEvents, Awaitable, Client } from 'discord.js' import type { ClientEvents, Awaitable, Client } from 'discord.js'
import { Ollama } from 'ollama'
// Export events through here to reduce amount of imports // Export events through here to reduce amount of imports
export { Events } from 'discord.js' export { Events } from 'discord.js'
@@ -6,16 +7,35 @@ export { Events } from 'discord.js'
export type LogMethod = (...args: unknown[]) => void export type LogMethod = (...args: unknown[]) => void
export type EventKeys = keyof ClientEvents // only wants keys of ClientEvents object export type EventKeys = keyof ClientEvents // only wants keys of ClientEvents object
/**
* Tokens to run the bot as intended
* @param channel the channel where the bot will respond to queries
* @param model chosen model for the ollama to utilize
* @param clientUid the discord id for the bot
*/
export type Tokens = {
channel: string,
model: string,
clientUid: string
}
/**
* Format for the messages to be stored when communicating when the bot
* @param role either assistant, user, or system
* @param content string of the message the user or assistant provided
*/
export type UserMessage = {
role: string,
content: string
}
// Event properties // Event properties
export interface EventProps { export interface EventProps {
client: Client client: Client
log: LogMethod log: LogMethod
msgHist: { role: string, content: string }[] msgHist: { role: string, content: string }[]
tokens: { tokens: Tokens,
channel: string, ollama: Ollama
model: string,
botUid: string
}
} }
export type EventCallback<T extends EventKeys> = ( export type EventCallback<T extends EventKeys> = (
props: EventProps, props: EventProps,
@@ -32,15 +52,20 @@ export function event<T extends EventKeys>(key: T, callback: EventCallback<T>):
return { key, callback } return { key, callback }
} }
/**
* Method to register events to the bot per file in the events directory
* @param client initialized bot client
* @param events all the exported events from the index.ts in the events dir
* @param msgHist The message history of the bot
* @param tokens the passed in environment tokens for the service
* @param ollama the initialized ollama instance
*/
export function registerEvents( export function registerEvents(
client: Client, client: Client,
events: Event[], events: Event[],
msgHist: { role: string, content: string }[], msgHist: UserMessage[],
tokens: { tokens: Tokens,
channel: string, ollama: Ollama
model: string,
botUid: string
}
): void { ): void {
for (const { key, callback } of events) { for (const { key, callback } of events) {
client.on(key, (...args) => { client.on(key, (...args) => {
@@ -49,7 +74,7 @@ export function registerEvents(
// Handle Errors, call callback, log errors as needed // Handle Errors, call callback, log errors as needed
try { try {
callback({ client, log, msgHist, tokens }, ...args) callback({ client, log, msgHist, tokens, ollama }, ...args)
} catch (error) { } catch (error) {
log('[Uncaught Error]', error) log('[Uncaught Error]', error)
} }

View File

@@ -2,4 +2,5 @@
export * from './env.js' export * from './env.js'
export * from './events.js' export * from './events.js'
export * from './messageEmbed.js' export * from './messageEmbed.js'
export * from './messageNormal.js' export * from './messageNormal.js'
export * from './commands.js'

56
src/utils/jsonHandler.ts Normal file
View File

@@ -0,0 +1,56 @@
import fs from 'fs'
export interface Configuration {
readonly name: string
options: {
'message-stream'?: boolean,
'message-style'?: boolean
}
}
/**
* Method to open a file in the working directory and modify/create it
*
* @param filename name of the file
* @param key key value to access
* @param value new value to assign
*/
export function openFile(filename: string, key: string, value: any) {
// check if the file exists, if not then make the config file
if (fs.existsSync(filename)) {
fs.readFile(filename, 'utf8', (error, data) => {
if (error)
console.log(`[Error: openFile] Incorrect file format`)
else {
const object = JSON.parse(data)
object['options'][key] = value
fs.writeFileSync(filename, JSON.stringify(object, null, 2))
}
})
} else {
const object: Configuration = JSON.parse('{ \"name\": \"Discord Ollama Confirgurations\" }')
// set standard information for config file and options
object['options'] = {
[key]: value
}
fs.writeFileSync(filename, JSON.stringify(object, null, 2))
console.log(`[Util: openFile] Created 'config.json' in working directory`)
}
}
export async function getConfig(filename: string, callback: (config: Configuration | undefined) => void): Promise<void> {
// attempt to read the file and get the configuration
if (fs.existsSync(filename)) {
fs.readFile(filename, 'utf8', (error, data) => {
if (error) {
callback(undefined)
return // something went wrong... stop
}
callback(JSON.parse(data))
})
} else {
callback(undefined) // file not found
}
}

View File

@@ -1,5 +1,6 @@
import { EmbedBuilder, Message } from "discord.js"; import { EmbedBuilder, Message } from 'discord.js'
import ollama, { ChatResponse } from "ollama"; import { ChatResponse, Ollama } from 'ollama'
import { UserMessage } from './events.js'
/** /**
* Method to send replies as normal text on discord like any other user * Method to send replies as normal text on discord like any other user
@@ -8,15 +9,13 @@ import ollama, { ChatResponse } from "ollama";
* @param msgHist message history between user and model * @param msgHist message history between user and model
*/ */
export async function embedMessage( export async function embedMessage(
message: Message, message: Message,
ollama: Ollama,
tokens: { tokens: {
channel: string, channel: string,
model: string model: string
}, },
msgHist: { msgHist: UserMessage[]
role: string,
content: string
}[]
) { ) {
// bot response // bot response
let response: ChatResponse let response: ChatResponse
@@ -44,17 +43,19 @@ export async function embedMessage(
stream: false stream: false
}) })
// dummy message to let user know that query is underway
const newEmbed = new EmbedBuilder() const newEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`) .setTitle(`Responding to ${message.author.tag}`)
.setDescription(response.message.content) .setDescription(response.message.content || 'No Content to Provide...')
.setColor('#00FF00') .setColor('#00FF00')
// edit the message // edit the message
sentMessage.edit({ embeds: [newEmbed] }) sentMessage.edit({ embeds: [newEmbed] })
} catch(error: any) { } catch(error: any) {
console.log(`[Util: messageEmbed] Error creating message: ${error.message}`)
const errorEmbed = new EmbedBuilder() const errorEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`) .setTitle(`Responding to ${message.author.tag}`)
.setDescription(error.error) .setDescription(`**Response generation failed.**\n\nReason: ${error.message}`)
.setColor('#00FF00') .setColor('#00FF00')
// send back error // send back error

View File

@@ -1,5 +1,6 @@
import { Message } from "discord.js"; import { Message } from 'discord.js'
import ollama, { ChatResponse } from "ollama"; import { ChatResponse, Ollama } from 'ollama'
import { UserMessage } from './events.js'
/** /**
* Method to send replies as normal text on discord like any other user * Method to send replies as normal text on discord like any other user
@@ -7,21 +8,19 @@ import ollama, { ChatResponse } from "ollama";
* @param tokens tokens to run query * @param tokens tokens to run query
* @param msgHist message history between user and model * @param msgHist message history between user and model
*/ */
export function normalMessage( export async function normalMessage(
message: Message, message: Message,
ollama: Ollama,
tokens: { tokens: {
channel: string, channel: string,
model: string model: string
}, },
msgHist: { msgHist: UserMessage[]
role: string,
content: string
}[]
) { ) {
// bot's respnse // bot's respnse
let response: ChatResponse let response: ChatResponse
message.reply('Generating Response . . .').then(async sentMessage => { await message.reply('Generating Response . . .').then(async sentMessage => {
try { try {
// Attempt to query model for message // Attempt to query model for message
response = await ollama.chat({ response = await ollama.chat({
@@ -39,7 +38,8 @@ export function normalMessage(
// edit the 'generic' response to new message // edit the 'generic' response to new message
sentMessage.edit(response.message.content) sentMessage.edit(response.message.content)
} catch(error: any) { } catch(error: any) {
sentMessage.edit(error.error) console.log(`[Util: messageNormal] Error creating message: ${error.message}`)
sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
} }
}) })

View File

@@ -1,7 +1,11 @@
import { AxiosResponse } from "axios"; import { AxiosResponse } from 'axios'
/** /**
* When running a /api/chat stream, the output needs to be parsed into an array of objects * When running a /api/chat stream, the output needs to be parsed into an array of objects
* This method is used for development purposes and testing
*
* This will not work as intended with the inclusion of ollama-js, needs to be modified to work with it
*
* @param stream Axios response to from Ollama * @param stream Axios response to from Ollama
*/ */
export function parseStream(stream: AxiosResponse<any, any>) { export function parseStream(stream: AxiosResponse<any, any>) {

View File

@@ -14,7 +14,8 @@
// We can import json files like JavaScript // We can import json files like JavaScript
"resolveJsonModule": true, "resolveJsonModule": true,
// Decompile .ts to .js into a folder named dist // Decompile .ts to .js into a folder named dist
"outDir": "dist" "outDir": "build",
"rootDir": "src"
}, },
// environment for env vars // environment for env vars
"include": ["src/**/*"], "include": ["src/**/*"],