3 Commits

Author SHA1 Message Date
JT2M0L3Y
cc7a3661b7 Update: fix imports based on last pkg fix 2025-02-23 21:24:35 -07:00
JT2M0L3Y
c026de5d62 Added: defined objects directory 2025-02-23 21:03:44 -07:00
JT2M0L3Y
f99bd2528d Update: utility method logs use method name 2025-02-23 21:03:44 -07:00
30 changed files with 1089 additions and 459 deletions

View File

@@ -13,3 +13,7 @@ DISCORD_IP = IP_ADDRESS
# subnet address, ex. 172.18.0.0 as we use /16. # subnet address, ex. 172.18.0.0 as we use /16.
SUBNET_ADDRESS = ADDRESS SUBNET_ADDRESS = ADDRESS
# redis port and ip, default redis port is 6379
REDIS_IP = IP_ADDRESS
REDIS_PORT = PORT

View File

@@ -33,7 +33,8 @@ jobs:
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
# set -e ensures if nohup fails, this section fails # set -e ensures if nohup fails, this section fails
- name: Startup Discord Bot Client - name: Startup Discord Bot Client
@@ -60,7 +61,8 @@ jobs:
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Setup Docker Network and Images - name: Setup Docker Network and Images
run: | run: |
@@ -68,8 +70,8 @@ jobs:
- name: Check Images Exist - name: Check Images Exist
run: | run: |
(docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama') || exit 1 (docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama' | docker images | grep -qE 'redis') || exit 1
- name: Check Containers Exist - name: Check Containers Exist
run: | run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord') || exit 1 (docker ps | grep -q 'ollama' && docker ps | grep -q 'discord' && docker ps | grep -q 'redis') || exit 1

View File

@@ -30,11 +30,12 @@ jobs:
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Collect Code Coverage - name: Collect Code Coverage
run: | run: |
LINE_PCT=$(npm run coverage | tail -2 | head -1 | awk '{print $3}') LINE_PCT=$(npm run test:coverage | tail -2 | head -1 | awk '{print $3}')
echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV
- name: Upload Code Coverage - name: Upload Code Coverage

View File

@@ -21,9 +21,10 @@ jobs:
echo CLIENT_TOKEN = ${{ secrets.CLIENT }} >> .env echo CLIENT_TOKEN = ${{ secrets.CLIENT }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo DISCORD_IP = ${{ secrets.DISCORD_IP }} >> .env echo DISCORD_IP = ${{ secrets.DISCORD_IP }} >> .env
echo SUBNET_ADDRESS = ${{ secrets.SUBNET_ADDRESS }} >> .env echo SUBNET_ADDRESS = ${{ secrets.SUBNET_ADDRESS }} >> .env
echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Check if directory exists and delete it - name: Check if directory exists and delete it
run: | run: |
@@ -57,6 +58,7 @@ jobs:
npm install npm install
IMAGE="kevinthedang/discord-ollama" IMAGE="kevinthedang/discord-ollama"
REDIS="redis"
OLLAMA="ollama/ollama" OLLAMA="ollama/ollama"
if docker images | grep -q $IMAGE; then if docker images | grep -q $IMAGE; then
@@ -72,6 +74,19 @@ jobs:
echo "Old $IMAGE Image Removed" echo "Old $IMAGE Image Removed"
fi fi
if docker images | grep -q $REDIS; then
IMAGE_ID=$(docker images -q $REDIS)
CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID")
if [ ! -z "$CONTAINER_IDS" ]; then
# Stop and remove the running containers
docker stop $CONTAINER_IDS
echo "Stopped and removed the containers using the image $REDIS"
fi
docker rmi $IMAGE_ID
echo "Old $REDIS Image Removed"
fi
if docker images | grep -q $OLLAMA; then if docker images | grep -q $OLLAMA; then
IMAGE_ID=$(docker images -q $OLLAMA) IMAGE_ID=$(docker images -q $OLLAMA)
CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID") CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID")
@@ -101,6 +116,14 @@ jobs:
--ip ${{ secrets.OLLAMA_IP }} \ --ip ${{ secrets.OLLAMA_IP }} \
ollama/ollama:latest ollama/ollama:latest
docker run --rm -d \
-v redis:/root/.redis \
-p ${{ secrets.REDIS_PORT }}:${{ secrets.REDIS_PORT }} \
--name redis \
--network ollama-net \
--ip ${{ secrets.REDIS_IP }} \
redis:latest
docker run --rm -d \ docker run --rm -d \
-v discord:/src/app \ -v discord:/src/app \
--name discord \ --name discord \

View File

@@ -41,8 +41,9 @@ jobs:
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Test Application - name: Test Application
run: | run: |
npm run tests npm run test:run

View File

@@ -23,6 +23,7 @@ The project aims to:
* [x] Generated Token Length Handling for >2000 * [x] Generated Token Length Handling for >2000
* [x] Token Length Handling of any message size * [x] Token Length Handling of any message size
* [x] User vs. Server Preferences * [x] User vs. Server Preferences
* [ ] Redis Caching
* [x] Administrator Role Compatible * [x] Administrator Role Compatible
* [x] Multi-User Chat Generation (Multiple users chatting at the same time) - This was built in from Ollama `v0.2.1+` * [x] Multi-User Chat Generation (Multiple users chatting at the same time) - This was built in from Ollama `v0.2.1+`
* [x] Automatic and Manual model pulling through the Discord client * [x] Automatic and Manual model pulling through the Discord client
@@ -53,6 +54,8 @@ These are guides to the features and capabilities of this app.
* This project requires the use of npm version `10.9.0` or above. * This project requires the use of npm version `10.9.0` or above.
* [Ollama](https://ollama.com/) * [Ollama](https://ollama.com/)
* [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama) * [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama)
* [Redis](https://redis.io/)
* [Redis Docker Image](https://hub.docker.com/_/redis)
* [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 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) * [Setting up Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)

View File

@@ -7,12 +7,13 @@ services:
build: ./ # find docker file in designated path build: ./ # find docker file in designated path
container_name: discord container_name: discord
restart: always # rebuild container always restart: always # rebuild container always
image: kevinthedang/discord-ollama:0.8.6 image: kevinthedang/discord-ollama:0.8.3
environment: environment:
CLIENT_TOKEN: ${CLIENT_TOKEN} CLIENT_TOKEN: ${CLIENT_TOKEN}
OLLAMA_IP: ${OLLAMA_IP} OLLAMA_IP: ${OLLAMA_IP}
OLLAMA_PORT: ${OLLAMA_PORT} OLLAMA_PORT: ${OLLAMA_PORT}
MODEL: ${MODEL} REDIS_IP: ${REDIS_IP}
REDIS_PORT: ${REDIS_PORT}
networks: networks:
ollama-net: ollama-net:
ipv4_address: ${DISCORD_IP} ipv4_address: ${DISCORD_IP}
@@ -35,6 +36,19 @@ services:
ports: ports:
- ${OLLAMA_PORT}:${OLLAMA_PORT} - ${OLLAMA_PORT}:${OLLAMA_PORT}
# setup redis container
redis:
image: redis:latest
container_name: redis
restart: always
networks:
ollama-net:
ipv4_address: ${REDIS_IP}
volumes:
- redis:/root/.redis
ports:
- ${REDIS_PORT}:${REDIS_PORT}
# create a network that supports giving addresses withing a specific subnet # create a network that supports giving addresses withing a specific subnet
networks: networks:
ollama-net: ollama-net:
@@ -47,3 +61,4 @@ networks:
volumes: volumes:
ollama: ollama:
discord: discord:
redis:

View File

@@ -43,11 +43,13 @@ sudo systemctl restart docker
* [GitHub repository](https://github.com/NVIDIA/nvidia-container-toolkit?tab=readme-ov-file) for Nvidia Container Toolkit * [GitHub repository](https://github.com/NVIDIA/nvidia-container-toolkit?tab=readme-ov-file) for Nvidia Container Toolkit
## To Run (with Docker and Docker Compose) ## 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: * With the inclusion of subnets in the `docker-compose.yml`, you will need to set the `SUBNET_ADDRESS`, `OLLAMA_IP`, `OLLAMA_PORT`, `REDIS_IP`, `REDIS_PORT`, and `DISCORD_IP`. Here are some default values if you don't care:
* `SUBNET_ADDRESS = 172.18.0.0` * `SUBNET_ADDRESS = 172.18.0.0`
* `OLLAMA_IP = 172.18.0.2` * `OLLAMA_IP = 172.18.0.2`
* `OLLAMA_PORT = 11434` * `OLLAMA_PORT = 11434`
* `DISCORD_IP = 172.18.0.3` * `DISCORD_IP = 172.18.0.3`
* `REDIS_IP = 172.18.0.4`
* `REDIS_PORT = 6379`
* Don't understand any of this? watch a Networking video to understand subnetting. * Don't understand any of this? watch a Networking video to understand subnetting.
* You also need all environment variables shown in [`.env.sample`](../.env.sample) * You also need all environment variables shown in [`.env.sample`](../.env.sample)
* 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. * 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.

1243
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,43 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.8.6", "version": "0.8.3",
"description": "Ollama Integration into discord", "description": "Ollama Integration into discord",
"main": "build/index.js", "main": "build/index.js",
"exports": "./build/index.js", "exports": "./build/index.js",
"scripts": { "scripts": {
"tests": "vitest run", "test:run": "vitest run",
"coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"watch": "tsx watch src", "watch": "tsx watch src",
"build": "tsc", "build": "tsc",
"prod": "node .", "prod": "node .",
"client": "npm run build && npm run prod", "client": "npm run build && npm run prod",
"clean": "docker compose down && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)", "clean": "docker compose down && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"start": "docker compose build --no-cache && docker compose up -d", "start": "docker compose build --no-cache && docker compose up -d",
"docker:clean": "docker rm -f discord && docker rm -f ollama && docker network prune -f && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)", "docker:clean": "docker rm -f discord && docker rm -f ollama && docker rm -f redis && docker network prune -f && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net", "docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net",
"docker:build": "docker build --no-cache -t kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\") .", "docker:build": "docker build --no-cache -t kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\") .",
"docker:build-latest": "docker build --no-cache -t kevinthedang/discord-ollama:latest .", "docker:build-latest": "docker build --no-cache -t kevinthedang/discord-ollama:latest .",
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\")", "docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\")",
"docker:redis": "docker run -d -v redis:/root/.redis -p 6379:6379 --name redis --network ollama-net --ip 172.18.0.4 redis:latest",
"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", "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",
"docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest", "docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest",
"docker:start": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama", "docker:start": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:redis && npm run docker:client && npm run docker:ollama",
"docker:start-cpu": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama-cpu" "docker:start-cpu": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:redis && npm run docker:client && npm run docker:ollama-cpu"
}, },
"author": "Kevin Dang", "author": "Kevin Dang",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.20.0", "discord.js": "^14.18.0",
"dotenv": "^16.5.0", "dotenv": "^16.4.7",
"ollama": "^0.5.15" "ollama": "^0.5.13",
"redis": "^4.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.5",
"@vitest/coverage-v8": "^3.0.9", "@vitest/coverage-v8": "^3.0.6",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.2", "typescript": "^5.7.3",
"vitest": "^3.0.4" "vitest": "^3.0.4"
}, },
"type": "module", "type": "module",

View File

@@ -1,6 +1,7 @@
import { Client, GatewayIntentBits } from 'discord.js' import { Client, GatewayIntentBits } from 'discord.js'
import { Ollama } from 'ollama' import { Ollama } from 'ollama'
import { Queue } from './queues/queue.js' import { createClient } from 'redis'
import { Queue } from './components/index.js'
import { UserMessage, registerEvents } from './utils/index.js' import { UserMessage, registerEvents } from './utils/index.js'
import Events from './events/index.js' import Events from './events/index.js'
import Keys from './keys.js' import Keys from './keys.js'
@@ -15,6 +16,11 @@ const client = new Client({
] ]
}) })
// initialize connection to redis
const redis = createClient({
url: `redis://${Keys.redisHost}:${Keys.redisPort}`,
})
// initialize connection to ollama container // initialize connection to ollama container
export const ollama = new Ollama({ export const ollama = new Ollama({
host: `http://${Keys.ipAddress}:${Keys.portAddress}`, host: `http://${Keys.ipAddress}:${Keys.portAddress}`,
@@ -26,6 +32,14 @@ const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
// register all events // register all events
registerEvents(client, Events, messageHistory, ollama, Keys.defaultModel) registerEvents(client, Events, messageHistory, ollama, Keys.defaultModel)
// Try to connect to redis
await redis.connect()
.then(() => console.log('[Redis] Connected'))
.catch((error) => {
console.error('[Redis] Connection Error', error)
process.exit(1)
})
// Try to log in the client // Try to log in the client
await client.login(Keys.clientToken) await client.login(Keys.clientToken)
.catch((error) => { .catch((error) => {

View File

@@ -1,4 +1,4 @@
import { Client, ChatInputCommandInteraction, ApplicationCommandOptionType, MessageFlags } from 'discord.js' import { Client, CommandInteraction, ApplicationCommandOptionType, MessageFlags } from 'discord.js'
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js' import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
export const Capacity: SlashCommand = { export const Capacity: SlashCommand = {
@@ -16,14 +16,14 @@ export const Capacity: SlashCommand = {
], ],
// Query for message information and set the style // Query for message information and set the style
run: async (client: Client, interaction: ChatInputCommandInteraction) => { run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
if (!channel || !UserCommand.includes(channel.type)) return if (!channel || !UserCommand.includes(channel.type)) return
// set state of bot chat features // set state of bot chat features
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, openConfig(`${interaction.user.username}-config.json`, interaction.commandName,
interaction.options.getNumber('context-capacity') interaction.options.get('context-capacity')?.value
) )
interaction.reply({ interaction.reply({

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, ChatInputCommandInteraction, Client, CommandInteraction, MessageFlags } from 'discord.js' import { ApplicationCommandOptionType, Client, CommandInteraction, MessageFlags } from 'discord.js'
import { UserCommand, SlashCommand } from '../utils/index.js' import { UserCommand, SlashCommand } from '../utils/index.js'
import { ollama } from '../client.js' import { ollama } from '../client.js'
import { ModelResponse } from 'ollama' import { ModelResponse } from 'ollama'
@@ -18,11 +18,10 @@ export const DeleteModel: SlashCommand = {
], ],
// Delete Model locally stored // Delete Model locally stored
run: async (client: Client, interaction: ChatInputCommandInteraction) => { run: async (client: Client, interaction: CommandInteraction) => {
// defer reply to avoid timeout // defer reply to avoid timeout
await interaction.deferReply() await interaction.deferReply()
const modelInput: string = interaction.options.getString('model-name') as string const modelInput: string = interaction.options.get('model-name')!!.value as string
let ollamaOffline: boolean = false
// fetch channel and message // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
@@ -38,22 +37,9 @@ export const DeleteModel: SlashCommand = {
} }
// check if model exists // check if model exists
const modelExists = await ollama.list() const modelExists: boolean = await ollama.list()
.then(response => response.models.some((model: ModelResponse) => model.name.startsWith(modelInput))) .then(response => response.models.some((model: ModelResponse) => model.name.startsWith(modelInput)))
.catch(error => {
ollamaOffline = true
console.error(`[Command: delete-model] Failed to connect with Ollama service. Error: ${error.message}`)
})
// Validate for any issue or if service is running
if (ollamaOffline) {
interaction.editReply({
content: `The Ollama service is not running. Please turn on/download the [service](https://ollama.com/).`
})
return
}
try { try {
// call ollama to delete model // call ollama to delete model
if (modelExists) { if (modelExists) {

View File

@@ -1,4 +1,4 @@
import { Client, ChatInputCommandInteraction, ApplicationCommandOptionType, MessageFlags } from 'discord.js' import { Client, CommandInteraction, ApplicationCommandOptionType, MessageFlags } from 'discord.js'
import { AdminCommand, openConfig, SlashCommand } from '../utils/index.js' import { AdminCommand, openConfig, SlashCommand } from '../utils/index.js'
export const Disable: SlashCommand = { export const Disable: SlashCommand = {
@@ -16,7 +16,7 @@ export const Disable: SlashCommand = {
], ],
// Query for message information and set the style // Query for message information and set the style
run: async (client: Client, interaction: ChatInputCommandInteraction) => { run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
if (!channel || !AdminCommand.includes(channel.type)) return if (!channel || !AdminCommand.includes(channel.type)) return
@@ -32,11 +32,11 @@ export const Disable: SlashCommand = {
// set state of bot chat features // set state of bot chat features
openConfig(`${interaction.guildId}-config.json`, interaction.commandName, openConfig(`${interaction.guildId}-config.json`, interaction.commandName,
interaction.options.getBoolean('enabled') interaction.options.get('enabled')?.value
) )
interaction.reply({ interaction.reply({
content: `${client.user?.username} is now **${interaction.options.getBoolean('enabled') ? "enabled" : "disabled"}**.`, content: `${client.user?.username} is now **${interaction.options.get('enabled')?.value ? "enabled" : "disabled"}**.`,
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral
}) })
} }

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction, MessageFlags } from 'discord.js' import { ApplicationCommandOptionType, Client, CommandInteraction, MessageFlags } from 'discord.js'
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js' import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
export const MessageStream: SlashCommand = { export const MessageStream: SlashCommand = {
@@ -16,18 +16,18 @@ export const MessageStream: SlashCommand = {
], ],
// change preferences based on command // change preferences based on command
run: async (client: Client, interaction: ChatInputCommandInteraction) => { run: async (client: Client, interaction: CommandInteraction) => {
// verify channel // verify channel
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
if (!channel || !UserCommand.includes(channel.type)) return if (!channel || !UserCommand.includes(channel.type)) return
// save value to json and write to it // save value to json and write to it
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, openConfig(`${interaction.user.username}-config.json`, interaction.commandName,
interaction.options.getBoolean('stream') interaction.options.get('stream')?.value
) )
interaction.reply({ interaction.reply({
content: `Message streaming is now set to: \`${interaction.options.getBoolean('stream')}\``, content: `Message streaming is now set to: \`${interaction.options.get('stream')?.value}\``,
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral
}) })
} }

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction, MessageFlags } from "discord.js" import { ApplicationCommandOptionType, Client, CommandInteraction, MessageFlags } from "discord.js"
import { ollama } from "../client.js" import { ollama } from "../client.js"
import { ModelResponse } from "ollama" import { ModelResponse } from "ollama"
import { UserCommand, SlashCommand } from "../utils/index.js" import { UserCommand, SlashCommand } from "../utils/index.js"
@@ -18,11 +18,10 @@ export const PullModel: SlashCommand = {
], ],
// Pull for model from Ollama library // Pull for model from Ollama library
run: async (client: Client, interaction: ChatInputCommandInteraction) => { run: async (client: Client, interaction: CommandInteraction) => {
// defer reply to avoid timeout // defer reply to avoid timeout
await interaction.deferReply() await interaction.deferReply()
const modelInput: string = interaction.options.getString('model-to-pull') as string const modelInput: string = interaction.options.get('model-to-pull')!!.value as string
let ollamaOffline: boolean = false
// fetch channel and message // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
@@ -37,22 +36,9 @@ export const PullModel: SlashCommand = {
return return
} }
// check if model was already pulled, if the ollama service isn't running throw error // check if model was already pulled
const modelExists = await ollama.list() const modelExists: boolean = await ollama.list()
.then(response => response.models.some((model: ModelResponse) => model.name.startsWith(modelInput))) .then(response => response.models.some((model: ModelResponse) => model.name.startsWith(modelInput)))
.catch(error => {
ollamaOffline = true
console.error(`[Command: pull-model] Failed to connect with Ollama service. Error: ${error.message}`)
})
// Validate for any issue or if service is running
if (ollamaOffline) {
interaction.editReply({
content: `The Ollama service is not running. Please turn on/download the [service](https://ollama.com/).`
})
return
}
try { try {
// call ollama to pull desired model // call ollama to pull desired model

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction } from "discord.js" import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js"
import { ollama } from "../client.js" import { ollama } from "../client.js"
import { ModelResponse } from "ollama" import { ModelResponse } from "ollama"
import { openConfig, UserCommand, SlashCommand } from "../utils/index.js" import { openConfig, UserCommand, SlashCommand } from "../utils/index.js"
@@ -18,10 +18,10 @@ export const SwitchModel: SlashCommand = {
], ],
// Switch user preferred model if available in local library // Switch user preferred model if available in local library
run: async (client: Client, interaction: ChatInputCommandInteraction) => { run: async (client: Client, interaction: CommandInteraction) => {
await interaction.deferReply() await interaction.deferReply()
const modelInput: string = interaction.options.getString('model-to-use') as string const modelInput: string = interaction.options.get('model-to-use')!!.value as string
// fetch channel and message // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
@@ -45,9 +45,6 @@ export const SwitchModel: SlashCommand = {
} }
} }
}) })
.catch(error => {
console.error(`[Command: switch-model] Failed to connect with Ollama service. Error: ${error.message}`)
})
// todo: problem can be here if async messes up // todo: problem can be here if async messes up
if (switchSuccess) { if (switchSuccess) {
// set model now that it exists // set model now that it exists
@@ -59,13 +56,10 @@ export const SwitchModel: SlashCommand = {
interaction.editReply({ interaction.editReply({
content: `Could not find **${modelInput}** in local model library.\n\nPlease contact an server admin for access to this model.` content: `Could not find **${modelInput}** in local model library.\n\nPlease contact an server admin for access to this model.`
}) })
} catch (error: any) { } catch (error) {
// could not resolve user model switch // could not resolve user model switch
if (error.message.includes("fetch failed") as string)
error.message = "The Ollama service is not running. Please turn on/download the [service](https://ollama.com/)."
interaction.editReply({ interaction.editReply({
content: `Unable to switch user preferred model to **${modelInput}**.\n\n${error.message}` content: `Unable to switch user preferred model to **${modelInput}**.\n\n${error}\n\nPossible solution is to request an server admin run \`/pull-model ${modelInput}\` and try again.`
}) })
return return
} }

46
src/components/binder.ts Normal file
View File

@@ -0,0 +1,46 @@
/**
* @class Logger
* @description A class to handle logging messages
* @method log
*/
export class Logger {
private logPrefix: string = ''
private type: string = 'log'
private constructPrefix(component?: string, method?: string): string {
let prefix = this.type.toUpperCase()
if (component) {
prefix += ` [${component}`
if (method) prefix += `: ${method}`
prefix += ']'
}
return prefix
}
public bind(component?: string, method?: string): CallableFunction {
let tempPrefix = this.constructPrefix(component, method)
if (tempPrefix !== this.logPrefix) this.logPrefix = tempPrefix
switch (this.type) {
case 'warn':
return console.warn.bind(console, this.logPrefix)
case 'error':
return console.error.bind(console, this.logPrefix)
case 'log':
default:
return console.log.bind(console, this.logPrefix)
}
}
public log(type: string, message: unknown, component?: string, method?: string): void {
if (type && type !== this.type) this.type = type
let log = this.bind(component, method)
log(message)
}
}

2
src/components/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './queue.js'
export * from './binder.js'

View File

@@ -1,6 +1,6 @@
import { TextChannel } from 'discord.js' import { TextChannel } from 'discord.js'
import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
import { import {
event, Events, normalMessage, UserMessage, clean,
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
} from '../utils/index.js' } from '../utils/index.js'
@@ -71,8 +71,9 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
userConfig = await new Promise((resolve, reject) => { userConfig = await new Promise((resolve, reject) => {
getUserConfig(`${message.author.username}-config.json`, (config) => { getUserConfig(`${message.author.username}-config.json`, (config) => {
if (config === undefined) { if (config === undefined) {
openConfig(`${message.author.username}-config.json`, 'message-style', false)
openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel) openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel)
reject(new Error(`No User Preferences is set up.\n\nCreating new preferences file for ${message.author.username}\nPlease try chatting again.`)) reject(new Error('No User Preferences is set up.\n\nCreating preferences file with \`message-style\` set as \`false\` for regular message style.\nPlease try chatting again.'))
return return
} }

View File

@@ -4,7 +4,9 @@ export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'), clientToken: getEnvVar('CLIENT_TOKEN'),
ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none
portAddress: getEnvVar('OLLAMA_PORT', '11434'), // default ollama port if none portAddress: getEnvVar('OLLAMA_PORT', '11434'), // default ollama port if none
defaultModel: getEnvVar('MODEL', 'llama3.2') defaultModel: getEnvVar('MODEL', 'llama3.2'),
redisHost: getEnvVar('REDIS_IP', '172.18.0.4'), // default redis host if none
redisPort: parseInt(getEnvVar('REDIS_PORT', '6379')) // default redis port if none
} as const // readonly keys } as const // readonly keys
export default Keys export default Keys

View File

@@ -1,4 +1,4 @@
import { ChatInputCommandInteraction, ChatInputApplicationCommandData, Client, ApplicationCommandOption } from 'discord.js' import { CommandInteraction, ChatInputApplicationCommandData, Client, ApplicationCommandOption } from 'discord.js'
/** /**
* interface for how slash commands should be run * interface for how slash commands should be run
@@ -6,7 +6,7 @@ import { ChatInputCommandInteraction, ChatInputApplicationCommandData, Client, A
export interface SlashCommand extends ChatInputApplicationCommandData { export interface SlashCommand extends ChatInputApplicationCommandData {
run: ( run: (
client: Client, client: Client,
interaction: ChatInputCommandInteraction, interaction: CommandInteraction,
options?: ApplicationCommandOption[] options?: ApplicationCommandOption[]
) => void ) => void
} }

View File

@@ -1,6 +1,6 @@
import type { ClientEvents, Awaitable, Client } from 'discord.js' import type { ClientEvents, Awaitable, Client } from 'discord.js'
import { Ollama } from 'ollama' import { Ollama } from 'ollama'
import { Queue } from '../queues/queue.js' import { Queue } from '../components/index.js'
// 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'

View File

@@ -64,7 +64,7 @@ export async function clearChannelInfo(filename: string, channel: TextChannel, u
* @param user the user's name * @param user the user's name
* @param messages their messages * @param messages their messages
*/ */
export async function openChannelInfo(filename: string, channel: TextChannel | ThreadChannel, user: string, messages: UserMessage[] = []): Promise<void> { export async function openChannelInfo(this: any, filename: string, channel: TextChannel | ThreadChannel, user: string, messages: UserMessage[] = []): Promise<void> {
const fullFileName = `data/${filename}-${user}.json` const fullFileName = `data/${filename}-${user}.json`
if (fs.existsSync(fullFileName)) { if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
@@ -95,7 +95,7 @@ export async function openChannelInfo(filename: string, channel: TextChannel | T
// only creating it, no need to add anything // only creating it, no need to add anything
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2)) fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
console.log(`[Util: openChannelInfo] Created '${fullFileName}' in working directory`) console.log(`[Util: ${this.name}] Created '${fullFileName}' in working directory`)
} }
} }

View File

@@ -10,7 +10,7 @@ import path from 'path'
* @param value new value to assign * @param value new value to assign
*/ */
// add type of change (server, user) // add type of change (server, user)
export function openConfig(filename: string, key: string, value: any) { export function openConfig(this: any, filename: string, key: string, value: any) {
const fullFileName = `data/${filename}` const fullFileName = `data/${filename}`
// check if the file exists, if not then make the config file // check if the file exists, if not then make the config file
@@ -41,7 +41,7 @@ export function openConfig(filename: string, key: string, value: any) {
fs.mkdirSync(directory, { recursive: true }) fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2)) fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2))
console.log(`[Util: openConfig] Created '${filename}' in working directory`) console.log(`[Util: ${this.name}] Created '${filename}' in working directory`)
} }
} }

View File

@@ -1,6 +1,5 @@
import { ChatResponse } from "ollama" import { ChatResponse, AbortableAsyncIterator } from "ollama"
import { ChatParams } from "../index.js" import { ChatParams } from "../index.js"
import { AbortableAsyncIterator } from "ollama/src/utils.js"
/** /**
* Method to query the Ollama client for async generation * Method to query the Ollama client for async generation

View File

@@ -1,8 +1,7 @@
import { Message, SendableChannels } from 'discord.js' import { Message, SendableChannels } from 'discord.js'
import { ChatResponse, Ollama } from 'ollama' import { ChatResponse, Ollama, AbortableAsyncIterator } from 'ollama'
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js' import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
import { Queue } from '../queues/queue.js' import { Queue } from '../components/index.js'
import { AbortableAsyncIterator } from 'ollama/src/utils.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
@@ -11,6 +10,7 @@ import { AbortableAsyncIterator } from 'ollama/src/utils.js'
* @param msgHist message history between user and model * @param msgHist message history between user and model
*/ */
export async function normalMessage( export async function normalMessage(
this: any,
message: Message, message: Message,
ollama: Ollama, ollama: Ollama,
model: string, model: string,
@@ -73,12 +73,11 @@ export async function normalMessage(
sentMessage.edit(result) sentMessage.edit(result)
} }
} catch (error: any) { } catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${error.message}`) console.log(`[Util: ${this.name}] Error creating message: ${error.message}`)
if (error.message.includes('fetch failed')) if (error.message.includes('try pulling it first'))
error.message = 'Missing ollama service on machine' sentMessage.edit(`**Response generation failed.**\n\nReason: You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.`)
else if (error.message.includes('try pulling it first')) else
error.message = `You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.` sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
} }
}) })

View File

@@ -5,9 +5,16 @@ import { describe, expect, it, vi } from 'vitest'
import commands from '../src/commands/index.js' import commands from '../src/commands/index.js'
/** /**
* Mocking client.ts because of the commands * Mocking redis found in client.ts because of the commands
*/ */
vi.mock('../src/client.js', () => ({})) vi.mock('../src/client.js', () => ({
redis: {
createClient: vi.fn(),
connect: vi.fn(),
get: vi.fn(),
set: vi.fn()
}
}))
/** /**
* Commands test suite, tests the commands object * Commands test suite, tests the commands object

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { Queue } from '../src/queues/queue.js' import { Queue } from '../src/components/index.js'
/** /**
* Queue test suite, tests the Queue class * Queue test suite, tests the Queue class