10 Commits

Author SHA1 Message Date
Kevin Dang
32137dacb0 Add: Filter out think tags in bot message (#190) 2025-10-04 20:17:26 -07:00
Kevin Dang
c00ea5de98 Additional Channel Awareness (#186) 2025-07-31 19:04:51 -07:00
Kevin Dang
b27cdfc162 Update Documentation with New Features (#185) 2025-06-22 20:43:50 -07:00
Jonathan Smoley
1074fe2270 Removed Redis Dependency (#184) 2025-06-20 17:04:56 -07:00
Jonathan Smoley
4236582cf4 Upgrade discord.js from 14.18.0 to 14.20.0 (#177)
* fix: upgrade discord.js from 14.18.0 to 14.19.3

Snyk has created this PR to upgrade discord.js from 14.18.0 to 14.19.3.

See this package in npm:
discord.js

See this project in Snyk:
https://app.snyk.io/org/jt2m0l3y/project/d8b070a3-e4a3-457a-977b-7eb6a4a48346?utm_source=github&utm_medium=referral&page=upgrade-pr

* Update: discordjs to latest

* Fix: Broken commands

* Fix: Ollama offline failsafes trigger

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Kevin Dang <kevinthedang_1@outlook.com>
2025-06-20 08:34:10 -07:00
Kevin Dang
e07e8fbf89 Fix Missing Redis Connections and Error Messages (#182)
* Update: simplify npm command to run tests

* Fix: redis workaround for local non docker

* Update: error message and config creation

* Fix: Better Messages for Ollama service being offline

* Update: version increment

* Fix: verion typo

* Update: Use built-in catch method for logging

* Update: same catch method for redis
2025-06-18 07:54:53 -07:00
Jonathan Smoley
6d0a537540 Upgrade dotenv to 16.5.0 (#173)
Snyk has created this PR to upgrade dotenv from 16.4.7 to 16.5.0.

See this package in npm:
dotenv

See this project in Snyk:
https://app.snyk.io/org/jt2m0l3y/project/d8b070a3-e4a3-457a-977b-7eb6a4a48346?utm_source=github&utm_medium=referral&page=upgrade-pr

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Kevin Dang <77701718+kevinthedang@users.noreply.github.com>
2025-05-15 07:07:06 -07:00
Jonathan Smoley
0ddd59aea1 Upgrade ollama package to 0.5.15 (#174)
Snyk has created this PR to upgrade ollama from 0.5.14 to 0.5.15.

See this package in npm:
ollama

See this project in Snyk:
https://app.snyk.io/org/jt2m0l3y/project/d8b070a3-e4a3-457a-977b-7eb6a4a48346?utm_source=github&utm_medium=referral&page=upgrade-pr

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2025-05-09 09:22:14 -07:00
Kevin Dang
a5faca87aa Fix: missing model env for docker (#172) 2025-04-18 19:42:18 -07:00
Jonathan Smoley
4c96b3863a Upgrade Dependencies (#164) 2025-03-28 10:00:50 -07:00
24 changed files with 568 additions and 1049 deletions

View File

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

View File

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

View File

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

View File

@@ -21,10 +21,9 @@ jobs:
echo CLIENT_TOKEN = ${{ secrets.CLIENT }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
echo DISCORD_IP = ${{ secrets.DISCORD_IP }} >> .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
run: |
@@ -58,7 +57,6 @@ jobs:
npm install
IMAGE="kevinthedang/discord-ollama"
REDIS="redis"
OLLAMA="ollama/ollama"
if docker images | grep -q $IMAGE; then
@@ -74,19 +72,6 @@ jobs:
echo "Old $IMAGE Image Removed"
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
IMAGE_ID=$(docker images -q $OLLAMA)
CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID")
@@ -116,14 +101,6 @@ jobs:
--ip ${{ secrets.OLLAMA_IP }} \
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 \
-v discord:/src/app \
--name discord \

View File

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

View File

@@ -14,19 +14,28 @@
Ollama is an AI model management tool that allows users to install and use custom large language models locally.
The project aims to:
* [x] Create a Discord bot that will utilize Ollama and chat to chat with users!
* [x] User Preferences on Chat
* [x] Message Persistance on Channels and Threads
* [x] Threads
* [x] Channels
* [x] User and Server Preferences
* [x] Message Persistance
* [x] Containerization with Docker
* [x] Slash Commands Compatible
* [ ] Summary Command
* [ ] Model Info Command
* [ ] List Models Command
* [x] Pull Model Command
* [x] Switch Model Command
* [x] Delete Model Command
* [x] Create Thread Command
* [x] Create Private Thread Command
* [x] Message Stream Command
* [x] Change Message History Size Command
* [x] Clear Channel History Command (User Only)
* [x] Administrator Role Compatible
* [x] Generated Token Length Handling for >2000
* [x] Token Length Handling of any message size
* [x] User vs. Server Preferences
* [ ] Redis Caching
* [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] Automatic and Manual model pulling through the Discord client
* [x] Multi-User Chat Generation - This was built in from Ollama `v0.2.1+`
* [ ] Ollama Tool Support Implementation
* [ ] Enhanced Channel Context Awareness
* [ ] Improved User Replied Triggers
Further, Ollama provides the functionality to utilize custom models or provide context for the top-layer of any model available through the Ollama model library.
* [Customize a model](https://github.com/ollama/ollama#customize-a-model)
@@ -54,8 +63,6 @@ These are guides to the features and capabilities of this app.
* This project requires the use of npm version `10.9.0` or above.
* [Ollama](https://ollama.com/)
* [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)
* [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)

View File

@@ -7,13 +7,12 @@ services:
build: ./ # find docker file in designated path
container_name: discord
restart: always # rebuild container always
image: kevinthedang/discord-ollama:0.8.3
image: kevinthedang/discord-ollama:0.8.7
environment:
CLIENT_TOKEN: ${CLIENT_TOKEN}
OLLAMA_IP: ${OLLAMA_IP}
OLLAMA_PORT: ${OLLAMA_PORT}
REDIS_IP: ${REDIS_IP}
REDIS_PORT: ${REDIS_PORT}
MODEL: ${MODEL}
networks:
ollama-net:
ipv4_address: ${DISCORD_IP}
@@ -36,19 +35,6 @@ services:
ports:
- ${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
networks:
ollama-net:
@@ -61,4 +47,3 @@ networks:
volumes:
ollama:
discord:
redis:

View File

@@ -43,13 +43,11 @@ sudo systemctl restart docker
* [GitHub repository](https://github.com/NVIDIA/nvidia-container-toolkit?tab=readme-ov-file) for Nvidia Container Toolkit
## 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`, `REDIS_IP`, `REDIS_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`, and `DISCORD_IP`. Here are some default values if you don't care:
* `SUBNET_ADDRESS = 172.18.0.0`
* `OLLAMA_IP = 172.18.0.2`
* `OLLAMA_PORT = 11434`
* `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.
* 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.

1243
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,41 @@
{
"name": "discord-ollama",
"version": "0.8.3",
"version": "0.8.7",
"description": "Ollama Integration into discord",
"main": "build/index.js",
"exports": "./build/index.js",
"scripts": {
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"tests": "vitest run",
"coverage": "vitest run --coverage",
"watch": "tsx watch src",
"build": "tsc",
"prod": "node .",
"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)",
"start": "docker compose build --no-cache && docker compose up -d",
"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: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: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-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: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-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: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:redis && npm run docker:client && npm run docker:ollama-cpu"
"docker:start": "docker network prune -f && npm run docker:network && npm run docker:build && 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"
},
"author": "Kevin Dang",
"license": "ISC",
"dependencies": {
"discord.js": "^14.18.0",
"dotenv": "^16.4.7",
"ollama": "^0.5.13",
"redis": "^4.7.0"
"discord.js": "^14.20.0",
"dotenv": "^16.5.0",
"ollama": "^0.5.15"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@vitest/coverage-v8": "^3.0.6",
"@types/node": "^22.13.14",
"@vitest/coverage-v8": "^3.0.9",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "^5.7.3",
"typescript": "^5.8.2",
"vitest": "^3.0.4"
},
"type": "module",

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, CommandInteraction, MessageFlags } from 'discord.js'
import { ApplicationCommandOptionType, ChatInputCommandInteraction, Client, CommandInteraction, MessageFlags } from 'discord.js'
import { UserCommand, SlashCommand } from '../utils/index.js'
import { ollama } from '../client.js'
import { ModelResponse } from 'ollama'
@@ -18,10 +18,11 @@ export const DeleteModel: SlashCommand = {
],
// Delete Model locally stored
run: async (client: Client, interaction: CommandInteraction) => {
run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// defer reply to avoid timeout
await interaction.deferReply()
const modelInput: string = interaction.options.get('model-name')!!.value as string
const modelInput: string = interaction.options.getString('model-name') as string
let ollamaOffline: boolean = false
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
@@ -37,8 +38,21 @@ export const DeleteModel: SlashCommand = {
}
// check if model exists
const modelExists: boolean = await ollama.list()
const modelExists = await ollama.list()
.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 {
// call ollama to delete model

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, CommandInteraction, MessageFlags } from "discord.js"
import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction, MessageFlags } from "discord.js"
import { ollama } from "../client.js"
import { ModelResponse } from "ollama"
import { UserCommand, SlashCommand } from "../utils/index.js"
@@ -18,10 +18,11 @@ export const PullModel: SlashCommand = {
],
// Pull for model from Ollama library
run: async (client: Client, interaction: CommandInteraction) => {
run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// defer reply to avoid timeout
await interaction.deferReply()
const modelInput: string = interaction.options.get('model-to-pull')!!.value as string
const modelInput: string = interaction.options.getString('model-to-pull') as string
let ollamaOffline: boolean = false
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
@@ -36,9 +37,22 @@ export const PullModel: SlashCommand = {
return
}
// check if model was already pulled
const modelExists: boolean = await ollama.list()
// check if model was already pulled, if the ollama service isn't running throw error
const modelExists = await ollama.list()
.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 {
// call ollama to pull desired model

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js"
import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction } from "discord.js"
import { ollama } from "../client.js"
import { ModelResponse } from "ollama"
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
run: async (client: Client, interaction: CommandInteraction) => {
run: async (client: Client, interaction: ChatInputCommandInteraction) => {
await interaction.deferReply()
const modelInput: string = interaction.options.get('model-to-use')!!.value as string
const modelInput: string = interaction.options.getString('model-to-use') as string
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
@@ -45,6 +45,9 @@ 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
if (switchSuccess) {
// set model now that it exists
@@ -56,10 +59,13 @@ export const SwitchModel: SlashCommand = {
interaction.editReply({
content: `Could not find **${modelInput}** in local model library.\n\nPlease contact an server admin for access to this model.`
})
} catch (error) {
} catch (error: any) {
// 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({
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.`
content: `Unable to switch user preferred model to **${modelInput}**.\n\n${error.message}`
})
return
}

View File

@@ -1,5 +1,5 @@
import { TextChannel } from 'discord.js'
import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
import { event, Events, normalMessage, UserMessage, clean, addToChannelContext } from '../utils/index.js'
import {
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
@@ -11,7 +11,7 @@ import {
*
* @param message the message received from the channel
*/
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => {
export default event(Events.MessageCreate, async ({ log, msgHist, channelHistory, ollama, client, defaultModel }, message) => {
const clientId = client.user!!.id
let cleanedMessage = clean(message.content, clientId)
log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
@@ -19,6 +19,61 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
// Do not respond if bot talks in the chat
if (message.author.username === message.client.user.username) return
// Save User Chat even if not for the bot
let channelContextHistory: UserMessage[] = await new Promise((resolve) => {
getChannelInfo(`${message.channelId}-context.json`, (channelInfo) => {
if (channelInfo?.messages)
resolve(channelInfo.messages)
else {
log(`Channel/Thread ${message.channel}-context does not exist. File will be created shortly...`)
resolve([])
}
})
})
if (channelContextHistory.length === 0) {
channelContextHistory = await new Promise((resolve) => {
addToChannelContext(message.channelId,
message.channel as TextChannel
)
getChannelInfo(`${message.channelId}-context.json`, (channelInfo) => {
if (channelInfo?.messages)
resolve(channelInfo.messages)
else {
log(`Channel/Thread ${message.channel}-context does not exist. File will be created shortly...`)
}
})
})
}
// Set Channel History Queue
channelHistory.setQueue(channelContextHistory)
// get message attachment if exists
const attachment = message.attachments.first()
let messageAttachment: string[] = []
if (attachment && attachment.name?.endsWith(".txt"))
cleanedMessage += ' ' + await getTextFileAttachmentData(attachment)
else if (attachment)
messageAttachment = await getAttachmentData(attachment)
while (channelHistory.size() >= channelHistory.capacity) channelHistory.dequeue()
// push user response to channel history
console.log
channelHistory.enqueue({
role: 'user',
content: cleanedMessage,
images: messageAttachment || []
})
// Store in Channel Context
addToChannelContext(message.channelId,
message.channel as TextChannel,
channelHistory.getItems()
)
// Only respond if message mentions the bot
if (!message.mentions.has(clientId)) return
@@ -71,9 +126,8 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
userConfig = await new Promise((resolve, reject) => {
getUserConfig(`${message.author.username}-config.json`, (config) => {
if (config === undefined) {
openConfig(`${message.author.username}-config.json`, 'message-style', false)
openConfig(`${message.author.username}-config.json`, 'switch-model', defaultModel)
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.'))
reject(new Error(`No User Preferences is set up.\n\nCreating new preferences file for ${message.author.username}\nPlease try chatting again.`))
return
}
@@ -140,15 +194,6 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
if (!userConfig)
throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`)
// get message attachment if exists
const attachment = message.attachments.first()
let messageAttachment: string[] = []
if (attachment && attachment.name?.endsWith(".txt"))
cleanedMessage += await getTextFileAttachmentData(attachment)
else if (attachment)
messageAttachment = await getAttachmentData(attachment)
const model: string = userConfig.options['switch-model']
// set up new queue

View File

@@ -4,9 +4,7 @@ export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'),
ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none
portAddress: getEnvVar('OLLAMA_PORT', '11434'), // default ollama port if none
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
defaultModel: getEnvVar('MODEL', 'llama3.2')
} as const // readonly keys
export default Keys

View File

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

View File

@@ -37,6 +37,7 @@ export interface EventProps {
client: Client,
log: LogMethod,
msgHist: Queue<UserMessage>,
channelHistory: Queue<UserMessage>,
ollama: Ollama,
defaultModel: String
}
@@ -78,6 +79,7 @@ export function registerEvents(
client: Client,
events: Event[],
msgHist: Queue<UserMessage>,
channelHistory: Queue<UserMessage>,
ollama: Ollama,
defaultModel: String
): void {
@@ -88,7 +90,7 @@ export function registerEvents(
// Handle Errors, call callback, log errors as needed
try {
callback({ client, log, msgHist, ollama, defaultModel }, ...args)
callback({ client, log, msgHist, channelHistory, ollama, defaultModel }, ...args)
} catch (error) {
log('[Uncaught Error]', error)
}

View File

@@ -56,6 +56,39 @@ export async function clearChannelInfo(filename: string, channel: TextChannel, u
return cleanedHistory
}
export async function addToChannelContext(filename: string, channel : TextChannel | ThreadChannel, messages: UserMessage[] = []): Promise<void> {
const fullFileName = `data/${filename}-context.json`
if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error)
console.log(`[Error: addToChannelContext] Incorrect file format`)
else {
const object = JSON.parse(data)
if (object['messages'].length === 0)
object['messages'] = messages as []
else if (object['messages'].length !== 0 && messages.length !== 0)
object['messages'] = messages as []
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
}
})
} else { // channel context does not exist, create it
const object: Configuration = JSON.parse(
`{
\"id\": \"${channel?.id}\",
\"name\": \"${channel?.name}\",
\"messages\": []
}`
)
const directory = path.dirname(fullFileName)
if (!fs.existsSync(directory))
fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
console.log(`[Util: addToChannelContext] Created '${fullFileName}' in working directory`)
}
}
/**
* Method to open the channel history
*

View File

@@ -56,6 +56,10 @@ export async function normalMessage(
response = await blockResponse(params)
result = response.message.content
// check if there is a <think>...</think> sequence from the bot.
if (hasThinking(result))
result = result.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
// check if message length > discord max for normal messages
if (result.length > 2000) {
sentMessage.edit(result.slice(0, 2000))
@@ -74,13 +78,18 @@ export async function normalMessage(
}
} catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${error.message}`)
if (error.message.includes('try pulling it first'))
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
sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
if (error.message.includes('fetch failed'))
error.message = 'Missing ollama service on machine'
else if (error.message.includes('try pulling it first'))
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}`)
}
})
// return the string representation of ollama query response
return result
}
function hasThinking(message: string): boolean {
return /<think>[\s\S]*?<\/think>/i.test(message)
}

View File

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