20 Commits

Author SHA1 Message Date
JT2M0L3Y
be6c64be82 Update: fix imports based on last pkg fix 2025-07-11 13:26:47 -07:00
JT2M0L3Y
427c1ecd3d Added: defined objects directory 2025-07-11 13:22:44 -07:00
JT2M0L3Y
5eda32b185 Update: utility method logs use method name 2025-07-11 13:22:38 -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
Kevin Dang
40783818b9 Upgrade Npm Packages (#159)
* Update: upgrade packages

* Update: add in all packages

* Update: fix whitespace in events

---------

Co-authored-by: JT2M0L3Y <jtsmoley@icloud.com>
2025-02-23 21:00:53 -07:00
Kevin Dang
ed0d8600df Deploy Badge (#163) 2025-02-22 15:23:01 -08:00
Kevin Dang
03939ef562 Server Deployment Scripts (#162) 2025-02-22 14:06:14 -08:00
Jonathan Smoley
456f70b9e1 Deprecated ephemeral field (#158)
* Update: ephemeral flag added in place of field

* Update: remove unused import

* Update: version increment

---------

Co-authored-by: Kevin Dang <kevinthedang_1@outlook.com>
2025-02-02 15:10:58 -08:00
Jonathan Smoley
5b542aca1a [Snyk] Upgrade discord.js from 14.16.3 to 14.17.3 (#155) 2025-01-31 16:23:31 -08:00
Kevin Dang
2a39e20fee Text Files As Prompts (#156)
* Add: .txt file reading

* Update: version increment
2025-01-31 14:12:11 -08:00
Jonathan Smoley
2ea77c92f0 Prepare Redis Environment (#133)
* add redis container

* Updated Guides and Goals  (#134)

* Update README.md

* Update commands-guide.md

* Update events-guide.md

* Update commands-guide.md

* Added: redis client

* Fixed: redis mock in commands.test.ts

* Updated: npm package patches

* Fixed: redis ip name in keys.ts

* update Node LTS version, workflow env vars

* Updated: node package engine requirements

* Updated: documentation

* fix: upgrade dotenv from 16.4.5 to 16.4.7 (#152)

Snyk has created this PR to upgrade dotenv from 16.4.5 to 16.4.7.

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>

* Update: docs patches, connection ordering

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-12-30 15:53:29 -08:00
Kevin Dang
6c7e48d369 Delete Model Command (#150)
* Add: Delete Model Command

* Update: version increment

* Update: new command to tests
2024-12-14 17:06:08 -08:00
Kevin Dang
fe1f7ce5ec Remove Message Style Command (#149)
* Remove: Message Style Command

* Update: version increment
2024-12-13 16:55:57 -08:00
48 changed files with 1128 additions and 1408 deletions

View File

@@ -13,10 +13,10 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/hydrogen node-version: lts/jod
cache: "npm" cache: "npm"
- name: Install Project Dependencies - name: Install Project Dependencies
@@ -33,6 +33,7 @@ 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
# 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
@@ -47,10 +48,10 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/hydrogen node-version: lts/jod
cache: "npm" cache: "npm"
- name: Create Environment Variables - name: Create Environment Variables
@@ -59,6 +60,7 @@ 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
- name: Setup Docker Network and Images - name: Setup Docker Network and Images
run: | run: |

View File

@@ -14,10 +14,10 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/hydrogen node-version: lts/jod
cache: "npm" cache: "npm"
- name: Install Project Dependencies - name: Install Project Dependencies
@@ -30,10 +30,11 @@ 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
- name: Collect Code Coverage - name: Collect Code Coverage
run: | 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 echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV
- name: Upload Code Coverage - name: Upload Code Coverage

109
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Deploy
run-name: Deploy Application Latest
on:
push:
tags:
- 'v*'
jobs:
Deploy-Application:
runs-on: self-hosted
environment: deploy
timeout-minutes: 5
steps:
- name: Checkout Repo
uses: actions/checkout@v4
# Generate Secret File for Compose case
- name: Create Environment Variables
run: |
touch .env
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
- name: Check if directory exists and delete it
run: |
if [ -d "${{ secrets.PATH }}" ]; then
echo "Directory exists, deleting old version..."
rm -rf ${{ secrets.PATH }}
else
echo "Directory does not exist."
fi
- name: Clone Repo onto Server
run: |
git clone https://github.com/kevinthedang/discord-ollama.git ${{ secrets.PATH }}
cd ${{ secrets.PATH }}
- name: Install nvm and Node.js lts/jod
run: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
echo "NVM installed successfully."
nvm install lts/jod
nvm alias default lts/jod
node -v
npm -v
- name: Build Application
run: |
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
npm install
IMAGE="kevinthedang/discord-ollama"
OLLAMA="ollama/ollama"
if docker images | grep -q $IMAGE; then
IMAGE_ID=$(docker images -q $IMAGE)
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 $IMAGE"
fi
docker rmi $IMAGE_ID
echo "Old $IMAGE 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")
if [ ! -z "$CONTAINER_IDS" ]; then
# Stop and remove the running containers
docker stop $CONTAINER_IDS
echo "Stopped and removed the containers using the image $OLLAMA"
fi
docker rmi $IMAGE_ID
echo "Old $OLLAMA Image Removed"
fi
docker network prune -f
docker system prune -a -f
npm run docker:build-latest
- name: Start Application
run: |
docker network create --subnet=${{ secrets.SUBNET_ADDRESS }}/16 ollama-net || true
docker run --rm -d \
-v ollama:/root/.ollama \
-p ${{ secrets.OLLAMA_PORT }}:${{ secrets.OLLAMA_PORT }} \
--name ollama \
--network ollama-net \
--ip ${{ secrets.OLLAMA_IP }} \
ollama/ollama:latest
docker run --rm -d \
-v discord:/src/app \
--name discord \
--network ollama-net \
--ip ${{ secrets.DISCORD_IP }} \
kevinthedang/discord-ollama

View File

@@ -1,48 +0,0 @@
name: Deploy
run-name: Release Docker Image
on:
push:
tags:
- 'v*'
jobs:
Release-Docker-Image:
runs-on: ubuntu-latest
environment: release
timeout-minutes: 3
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen
uses: actions/setup-node@v4
with:
node-version: lts/hydrogen
cache: "npm"
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = NOT_REAL_TOKEN >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Get Version from package.json
run: echo "VERSION=$(jq -r '.version' package.json)" >> $GITHUB_ENV
- name: Build Image
run: |
npm run docker:build
- name: Build Image as Latest
run: |
npm run docker:build-latest
- name: Log into Docker
run: |
docker login --username ${{ vars.DOCKER_USER }} --password ${{ secrets.DOCKER_PASS }}
- name: Release Docker Image
run: |
docker push ${{ vars.DOCKER_USER }}/discord-ollama:${{ env.VERSION }}
docker push ${{ vars.DOCKER_USER }}/discord-ollama:latest

View File

@@ -25,10 +25,10 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/hydrogen - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/hydrogen node-version: lts/jod
cache: "npm" cache: "npm"
- name: Install Project Dependencies - name: Install Project Dependencies
@@ -41,7 +41,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
- name: Test Application - name: Test Application
run: | run: |
npm run test:run npm run tests

View File

@@ -11,7 +11,7 @@
# Technical/Business Code Ownership # Technical/Business Code Ownership
/src/ @kevinthedang @JT2M0L3Y /src/ @kevinthedang @JT2M0L3Y
/tests/ @kevinthedang @JT2M0L3Y /tests/ @JT2M0L3Y
/.github/ @kevinthedang /.github/ @kevinthedang
# Docker Ownership # Docker Ownership

View File

@@ -1,5 +1,5 @@
# use node LTS image for version 18 # use node LTS image for version 22
FROM node:hydrogen-alpine FROM node:jod-alpine
# set working directory inside container # set working directory inside container
WORKDIR /app WORKDIR /app

View File

@@ -5,7 +5,7 @@
<p><a href="#"></a><a href="https://creativecommons.org/licenses/by/4.0/"><img alt="License" src="https://img.shields.io/badge/License-CC_BY_4.0-darkgreen.svg" /></a> <p><a href="#"></a><a href="https://creativecommons.org/licenses/by/4.0/"><img alt="License" src="https://img.shields.io/badge/License-CC_BY_4.0-darkgreen.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/release.yml"><img alt="Release Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/release.yml/badge.svg" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/deploy.yml"><img alt="Deploy Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/deploy.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml"><img alt="Testing Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml/badge.svg" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml"><img alt="Testing Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/coverage.yml"><img alt="Code Coverage" src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kevinthedang/bc7b5dcfa16561ab02bb3df67a99b22d/raw/coverage.json"></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/coverage.yml"><img alt="Code Coverage" src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kevinthedang/bc7b5dcfa16561ab02bb3df67a99b22d/raw/coverage.json"></a>
</div> </div>
@@ -14,25 +14,35 @@
Ollama is an AI model management tool that allows users to install and use custom large language models locally. Ollama is an AI model management tool that allows users to install and use custom large language models locally.
The project aims to: The project aims to:
* [x] Create a Discord bot that will utilize Ollama and chat to chat with users! * [x] Create a Discord bot that will utilize Ollama and chat to chat with users!
* [x] User Preferences on Chat * [x] User and Server Preferences
* [x] Message Persistance on Channels and Threads * [x] Message Persistance
* [x] Threads
* [x] Channels
* [x] Containerization with Docker * [x] Containerization with Docker
* [x] Slash Commands Compatible * [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] 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] Multi-User Chat Generation - This was built in from Ollama `v0.2.1+`
* [ ] Redis Caching * [ ] Ollama Tool Support Implementation
* [x] Administrator Role Compatible * [ ] Enhanced Channel Context Awareness
* [x] Multi-User Chat Generation (Multiple users chatting at the same time) - This was built in from Ollama `v0.2.1+` * [ ] Improved User Replied Triggers
* [x] Automatic and Manual model pulling through the Discord client
* [ ] Allow others to create their own models personalized for their own servers! 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.
* [ ] Documentation on creating your own LLM * [Customize a model](https://github.com/ollama/ollama#customize-a-model)
* [ ] Documentation on web scrapping and cleaning * [Modelfile Docs](https://github.com/ollama/ollama/blob/main/docs/modelfile.md)
## Documentation ## Documentation
These are guides to the feature set included and the events triggered in this app. These are guides to the features and capabilities of this app.
* [User Slash Commands](./docs/commands-guide.md) * [User Slash Commands](./docs/commands-guide.md)
* [Client Events](./docs/events-guide.md) * [Client Events](./docs/events-guide.md)
@@ -49,8 +59,8 @@ These are guides to the feature set included and the events triggered in this ap
## Resources ## Resources
* [NodeJS](https://nodejs.org/en) * [NodeJS](https://nodejs.org/en)
* This project runs on `lts\hydrogen`. * This project runs on `lts\jod` and above.
* This project supports any NodeJS version above `16.x.x` to only allow ESModules. * 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)
* [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main) * [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main)

View File

@@ -7,11 +7,12 @@ 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.7.4 image: kevinthedang/discord-ollama:0.8.6
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}
networks: networks:
ollama-net: ollama-net:
ipv4_address: ${DISCORD_IP} ipv4_address: ${DISCORD_IP}
@@ -26,7 +27,6 @@ services:
networks: networks:
ollama-net: ollama-net:
ipv4_address: ${OLLAMA_IP} ipv4_address: ${OLLAMA_IP}
runtime: nvidia # use Nvidia Container Toolkit for GPU support runtime: nvidia # use Nvidia Container Toolkit for GPU support
devices: devices:
- /dev/nvidia0 - /dev/nvidia0
@@ -35,8 +35,6 @@ services:
ports: ports:
- ${OLLAMA_PORT}:${OLLAMA_PORT} - ${OLLAMA_PORT}:${OLLAMA_PORT}
# Put Redis Container here?
# 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:

View File

@@ -78,8 +78,7 @@ This is a guide to all of the slash commands for the app.
``` ```
/message-stream stream true /message-stream stream true
``` ```
> [!NOTE] **This is very slow on Discord because "spamming" changes in a channel within a period of 5 seconds is not allowed.**
> This is a very slow progress on Discord because "spamming" changes within 5 seconds is not allowed.
3. Message Style 3. Message Style
This command allows a user to select whether to embed the app's response. This command allows a user to select whether to embed the app's response.

View File

@@ -1,7 +1,9 @@
## Events Guide ## Events Guide
This is a guide to all of the client events for the app. This is a guide to all of the client events for the app.
> [!NOTE] Each of these is logged to the console for a developer to track. > [!NOTE]
> * Each of these is logged to the console for a developer to track.
> * Possible interactions include commands, buttons, menus, etc.
1. ClientReady 1. ClientReady
This event signifies that the Discord app is online. This event signifies that the Discord app is online.
@@ -11,8 +13,6 @@ This is a guide to all of the client events for the app.
This event signifies that a user interacted from Discord in some way. This event signifies that a user interacted from Discord in some way.
Here commands are selected from a knowledge bank and executed if found. Here commands are selected from a knowledge bank and executed if found.
> [!NOTE] Possible interactions include commands, buttons, menus, etc.
3. MessageCreate 3. MessageCreate
This event signifies that a message was sent. This event signifies that a message was sent.
Here user questions and comments for the LLM are processed. Here user questions and comments for the LLM are processed.
@@ -24,4 +24,4 @@ This is a guide to all of the client events for the app.
4. ThreadDelete 4. ThreadDelete
This event signifies that a Discord Thread was deleted. This event signifies that a Discord Thread was deleted.
Here any preferences set for interaction within the thread are cleared away. Here any preferences set for interaction within the thread are cleared away.

View File

@@ -44,10 +44,10 @@ sudo systemctl restart docker
## 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`, 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_IP = 172.18.0.2`
* `OLLAMA_PORT = 11434` * `OLLAMA_PORT = 11434`
* `DISCORD_IP = 172.18.0.3` * `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. * 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.

1367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.7.4", "version": "0.8.6",
"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": {
"test:run": "vitest run", "tests": "vitest run",
"test:coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"watch": "tsx watch src", "watch": "tsx watch src",
"build": "tsc", "build": "tsc",
"prod": "node .", "prod": "node .",
@@ -26,20 +26,21 @@
"author": "Kevin Dang", "author": "Kevin Dang",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.16.3", "discord.js": "^14.20.0",
"dotenv": "^16.4.5", "dotenv": "^16.5.0",
"ollama": "^0.5.9" "ollama": "^0.5.15"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.9.0", "@types/node": "^22.13.14",
"@vitest/coverage-v8": "^2.1.4", "@vitest/coverage-v8": "^3.0.9",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.2", "tsx": "^4.19.3",
"typescript": "^5.6.3", "typescript": "^5.8.2",
"vitest": "^2.1.4" "vitest": "^3.0.4"
}, },
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=16.0.0" "npm": ">=10.9.0",
"node": ">=22.12.0"
} }
} }

View File

@@ -1,42 +1,41 @@
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 { 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'
// 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({ intents: [
intents: [ GatewayIntentBits.Guilds,
GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent
GatewayIntentBits.MessageContent ]
] })
});
// 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}`, })
})
// Create Queue managed by Events
// Create Queue managed by Events const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
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 log in the client
// Try to log in the client await 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) })
})
// queue up bots name
// queue up bots name messageHistory.enqueue({
messageHistory.enqueue({ role: 'assistant',
role: 'assistant', content: `My name is ${client.user?.username}`,
content: `My name is ${client.user?.username}`, images: []
images: []
}) })

View File

@@ -1,4 +1,4 @@
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js' import { Client, ChatInputCommandInteraction, 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,17 +16,19 @@ 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: CommandInteraction) => { run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// 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, interaction.options.get('context-capacity')?.value) openConfig(`${interaction.user.username}-config.json`, interaction.commandName,
interaction.options.getNumber('context-capacity')
)
interaction.reply({ interaction.reply({
content: `Max message history is now set to \`${interaction.options.get('context-capacity')?.value}\``, content: `Max message history is now set to \`${interaction.options.get('context-capacity')?.value}\``,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
} }
} }

View File

@@ -1,4 +1,4 @@
import { Channel, Client, CommandInteraction, TextChannel } from 'discord.js' import { Channel, Client, CommandInteraction, MessageFlags, TextChannel } from 'discord.js'
import { clearChannelInfo, SlashCommand, UserCommand } from '../utils/index.js' import { clearChannelInfo, SlashCommand, UserCommand } from '../utils/index.js'
export const ClearUserChannelHistory: SlashCommand = { export const ClearUserChannelHistory: SlashCommand = {
@@ -14,20 +14,22 @@ export const ClearUserChannelHistory: SlashCommand = {
if (!channel || !UserCommand.includes(channel.type)) return if (!channel || !UserCommand.includes(channel.type)) return
// clear channel info for user // clear channel info for user
const successfulWipe = await clearChannelInfo(interaction.channelId, const successfulWipe = await clearChannelInfo(
interaction.channel as TextChannel, interaction.channelId,
interaction.user.username) interaction.channel as TextChannel,
interaction.user.username
)
// check result of clearing history // check result of clearing history
if (successfulWipe) if (successfulWipe)
interaction.reply({ interaction.reply({
content: `History cleared in **this channel** cleared for **${interaction.user.username}**.`, content: `History cleared in **this channel** cleared for **${interaction.user.username}**.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
else else
interaction.reply({ interaction.reply({
content: `History was not be found for **${interaction.user.username}** in **this channel**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`, content: `History was not be found for **${interaction.user.username}** in **this channel**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
} }
} }

View File

@@ -0,0 +1,74 @@
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'
export const DeleteModel: SlashCommand = {
name: 'delete-model',
description: 'deletes a model from the local list of models. Administrator Only.',
// set available user options to pass to the command
options: [
{
name: 'model-name',
description: 'the name of the model to delete',
type: ApplicationCommandOptionType.String,
required: true
}
],
// Delete Model locally stored
run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// defer reply to avoid timeout
await interaction.deferReply()
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)
if (!channel || !UserCommand.includes(channel.type)) return
// Admin Command
if (!interaction.memberPermissions?.has('Administrator')) {
interaction.reply({
content: `${interaction.commandName} is an admin command.\n\nPlease contact a server admin to pull the model you want.`,
flags: MessageFlags.Ephemeral
})
return
}
// check if model exists
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
if (modelExists) {
await ollama.delete({ model: modelInput })
interaction.editReply({
content: `**${modelInput}** was removed from the the library.`
})
} else
throw new Error()
} catch (error) {
// could not delete the model
interaction.reply({
content: `Could not delete the **${modelInput}** model. It probably doesn't exist or you spelled it incorrectly.\n\nPlease try again if this is a mistake.`,
flags: MessageFlags.Ephemeral
})
}
}
}

View File

@@ -1,4 +1,4 @@
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js' import { Client, ChatInputCommandInteraction, 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: CommandInteraction) => { run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// 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
@@ -25,17 +25,19 @@ export const Disable: SlashCommand = {
if (!interaction.memberPermissions?.has('Administrator')) { if (!interaction.memberPermissions?.has('Administrator')) {
interaction.reply({ interaction.reply({
content: `${interaction.commandName} is an admin command.\n\nPlease contact an admin to use this command for you.`, content: `${interaction.commandName} is an admin command.\n\nPlease contact an admin to use this command for you.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
return return
} }
// set state of bot chat features // set state of bot chat features
openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('enabled')?.value) openConfig(`${interaction.guildId}-config.json`, interaction.commandName,
interaction.options.getBoolean('enabled')
)
interaction.reply({ 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"}**.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
} }
} }

View File

@@ -1,6 +1,5 @@
import { SlashCommand } from '../utils/commands.js' import { SlashCommand } from '../utils/commands.js'
import { ThreadCreate } from './threadCreate.js' import { ThreadCreate } from './threadCreate.js'
import { MessageStyle } from './messageStyle.js'
import { MessageStream } from './messageStream.js' import { MessageStream } from './messageStream.js'
import { Disable } from './disable.js' import { Disable } from './disable.js'
import { Shutoff } from './shutoff.js' import { Shutoff } from './shutoff.js'
@@ -9,16 +8,17 @@ import { PrivateThreadCreate } from './threadPrivateCreate.js'
import { ClearUserChannelHistory } from './cleanUserChannelHistory.js' import { ClearUserChannelHistory } from './cleanUserChannelHistory.js'
import { PullModel } from './pullModel.js' import { PullModel } from './pullModel.js'
import { SwitchModel } from './switchModel.js' import { SwitchModel } from './switchModel.js'
import { DeleteModel } from './deleteModel.js'
export default [ export default [
ThreadCreate, ThreadCreate,
PrivateThreadCreate, PrivateThreadCreate,
MessageStyle,
MessageStream, MessageStream,
Disable, Disable,
Shutoff, Shutoff,
Capacity, Capacity,
ClearUserChannelHistory, ClearUserChannelHistory,
PullModel, PullModel,
SwitchModel SwitchModel,
DeleteModel
] as SlashCommand[] ] as SlashCommand[]

View File

@@ -1,4 +1,4 @@
import { ApplicationCommandOptionType, Client, CommandInteraction } from 'discord.js' import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction, 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,17 +16,19 @@ export const MessageStream: SlashCommand = {
], ],
// change preferences based on command // change preferences based on command
run: async (client: Client, interaction: CommandInteraction) => { run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// 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, interaction.options.get('stream')?.value) openConfig(`${interaction.user.username}-config.json`, interaction.commandName,
interaction.options.getBoolean('stream')
)
interaction.reply({ 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')}\``,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
} }
} }

View File

@@ -1,32 +0,0 @@
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { openConfig, SlashCommand, UserCommand } from '../utils/index.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 || !UserCommand.includes(channel.type)) return
// set the message style
openConfig(`${interaction.user.username}-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

@@ -1,12 +1,11 @@
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js"; import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction, MessageFlags } from "discord.js"
import { SlashCommand } from "../utils/commands.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 } from "../utils/index.js";
export const PullModel: SlashCommand = { export const PullModel: SlashCommand = {
name: 'pull-model', name: 'pull-model',
description: 'pulls a model from the ollama model library. Administrator Only', description: 'pulls a model from the ollama model library. Administrator Only.',
// set available user options to pass to the command // set available user options to pass to the command
options: [ options: [
@@ -19,10 +18,11 @@ export const PullModel: SlashCommand = {
], ],
// Pull for model from Ollama library // Pull for model from Ollama library
run: async (client: Client, interaction: CommandInteraction) => { run: async (client: Client, interaction: ChatInputCommandInteraction) => {
// defer reply to avoid timeout // defer reply to avoid timeout
await interaction.deferReply() 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 // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
@@ -32,14 +32,27 @@ export const PullModel: SlashCommand = {
if (!interaction.memberPermissions?.has('Administrator')) { if (!interaction.memberPermissions?.has('Administrator')) {
interaction.reply({ interaction.reply({
content: `${interaction.commandName} is an admin command.\n\nPlease contact a server admin to pull the model you want.`, content: `${interaction.commandName} is an admin command.\n\nPlease contact a server admin to pull the model you want.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
return return
} }
// check if model was already pulled // check if model was already pulled, if the ollama service isn't running throw error
const modelExists: boolean = await ollama.list() const modelExists = 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,6 +1,5 @@
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js' import { Client, CommandInteraction, MessageFlags } from 'discord.js'
import { SlashCommand } from '../utils/commands.js' import { AdminCommand, SlashCommand } from '../utils/index.js'
import { AdminCommand } from '../utils/index.js'
export const Shutoff: SlashCommand = { export const Shutoff: SlashCommand = {
name: 'shutoff', name: 'shutoff',
@@ -19,7 +18,7 @@ export const Shutoff: SlashCommand = {
if (!interaction.memberPermissions?.has('Administrator')) { if (!interaction.memberPermissions?.has('Administrator')) {
interaction.reply({ interaction.reply({
content: `**Shutdown Aborted:**\n\n${interaction.user.tag}, You do not have permission to shutoff **${client.user?.tag}**.`, content: `**Shutdown Aborted:**\n\n${interaction.user.tag}, You do not have permission to shutoff **${client.user?.tag}**.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
return // stop from shutting down return // stop from shutting down
} }
@@ -27,8 +26,9 @@ export const Shutoff: SlashCommand = {
// Shutoff cleared, do it // Shutoff cleared, do it
interaction.reply({ interaction.reply({
content: `${client.user?.tag} is shutting down.`, content: `${client.user?.tag} is shutting down.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
console.log(`[Command: shutoff] ${client.user?.tag} is shutting down.`) console.log(`[Command: shutoff] ${client.user?.tag} is shutting down.`)
// clean up client instance and stop // clean up client instance and stop

View File

@@ -1,8 +1,7 @@
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js"; import { ApplicationCommandOptionType, Client, ChatInputCommandInteraction } from "discord.js"
import { SlashCommand } from "../utils/commands.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 } from "../utils/index.js";
export const SwitchModel: SlashCommand = { export const SwitchModel: SlashCommand = {
name: 'switch-model', name: 'switch-model',
@@ -19,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: CommandInteraction) => { run: async (client: Client, interaction: ChatInputCommandInteraction) => {
await interaction.deferReply() 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 // fetch channel and message
const channel = await client.channels.fetch(interaction.channelId) const channel = await client.channels.fetch(interaction.channelId)
@@ -32,20 +31,23 @@ export const SwitchModel: SlashCommand = {
// Phase 1: Switch to the model // Phase 1: Switch to the model
let switchSuccess = false let switchSuccess = false
await ollama.list() await ollama.list()
.then(response => { .then(response => {
for (const model in response.models) { for (const model in response.models) {
const currentModel: ModelResponse = response.models[model] const currentModel: ModelResponse = response.models[model]
if (currentModel.name.startsWith(modelInput)) { if (currentModel.name.startsWith(modelInput)) {
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, modelInput) openConfig(`${interaction.user.username}-config.json`, interaction.commandName, modelInput)
// successful switch // successful switch
interaction.editReply({ interaction.editReply({
content: `Successfully switched to **${modelInput}** as the preferred model for ${interaction.user.username}.` content: `Successfully switched to **${modelInput}** as the preferred model for ${interaction.user.username}.`
}) })
switchSuccess = true switchSuccess = true
}
} }
} })
}) .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
@@ -56,11 +58,14 @@ export const SwitchModel: SlashCommand = {
// Phase 2: Notify user of failure to find model. // Phase 2: Notify user of failure to find model.
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) { } catch (error: any) {
// 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}\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 return
} }

View File

@@ -1,4 +1,4 @@
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js' import { ChannelType, Client, CommandInteraction, MessageFlags, TextChannel, ThreadChannel } from 'discord.js'
import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js' import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js'
export const ThreadCreate: SlashCommand = { export const ThreadCreate: SlashCommand = {
@@ -21,14 +21,12 @@ export const ThreadCreate: SlashCommand = {
thread.send(`Hello ${interaction.user} and others! \n\nIt's nice to meet you. Please talk to me by typing **@${client.user?.username}** with your message.`) thread.send(`Hello ${interaction.user} and others! \n\nIt's nice to meet you. Please talk to me by typing **@${client.user?.username}** with your message.`)
// handle storing this chat channel // handle storing this chat channel
openChannelInfo(thread.id, openChannelInfo(thread.id, thread as ThreadChannel, interaction.user.tag)
thread as ThreadChannel,
interaction.user.tag)
// user only reply // user only reply
return interaction.reply({ return interaction.reply({
content: `I can help you in <#${thread.id}> below.`, content: `I can help you in <#${thread.id}> below.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
} }
} }

View File

@@ -1,4 +1,4 @@
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js' import { ChannelType, Client, CommandInteraction, MessageFlags, TextChannel, ThreadChannel } from 'discord.js'
import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js' import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js'
export const PrivateThreadCreate: SlashCommand = { export const PrivateThreadCreate: SlashCommand = {
@@ -22,15 +22,12 @@ export const PrivateThreadCreate: SlashCommand = {
// handle storing this chat channel // handle storing this chat channel
// store: thread.id, thread.name // store: thread.id, thread.name
openChannelInfo(thread.id, openChannelInfo(thread.id, thread as ThreadChannel, interaction.user.tag)
thread as ThreadChannel,
interaction.user.tag
)
// user only reply // user only reply
return interaction.reply({ return interaction.reply({
content: `I can help you in <#${thread.id}>.`, content: `I can help you in <#${thread.id}>.`,
ephemeral: true flags: MessageFlags.Ephemeral
}) })
} }
} }

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

@@ -17,7 +17,7 @@ export class Queue<T> implements IQueue<T> {
* Set up Queue * Set up Queue
* @param capacity max length of queue * @param capacity max length of queue
*/ */
constructor(public capacity: number = 5) {} constructor(public capacity: number = 5) { }
/** /**
* Put item in front of queue * Put item in front of queue

View File

@@ -7,7 +7,7 @@ import commands from '../commands/index.js'
*/ */
export default event(Events.InteractionCreate, async ({ log, client }, interaction) => { export default event(Events.InteractionCreate, async ({ log, client }, interaction) => {
if (!interaction.isCommand() || !interaction.isChatInputCommand()) return if (!interaction.isCommand() || !interaction.isChatInputCommand()) return
log(`Interaction called \'${interaction.commandName}\' from ${interaction.user.tag}.`) log(`Interaction called \'${interaction.commandName}\' from ${interaction.user.tag}.`)
// ensure command exists, otherwise kill event // ensure command exists, otherwise kill event

View File

@@ -1,6 +1,9 @@
import { TextChannel } from 'discord.js' import { TextChannel } from 'discord.js'
import { embedMessage, event, Events, normalMessage, UserMessage, clean } from '../utils/index.js' import {
import { getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, openConfig, UserConfig, getAttachmentData } from '../utils/index.js' event, Events, normalMessage, UserMessage, clean,
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
} from '../utils/index.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).
@@ -10,7 +13,7 @@ import { getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, openCo
*/ */
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => { export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => {
const clientId = client.user!!.id const clientId = client.user!!.id
const cleanedMessage = clean(message.content, clientId) let cleanedMessage = clean(message.content, clientId)
log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`) log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
// Do not respond if bot talks in the chat // Do not respond if bot talks in the chat
@@ -25,7 +28,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
// Params for Preferences Fetching // Params for Preferences Fetching
const maxRetries = 3 const maxRetries = 3
const delay = 1000 // in millisecons const delay = 1000 // in millisecons
try { try {
// Retrieve Server/Guild Preferences // Retrieve Server/Guild Preferences
let attempt = 0 let attempt = 0
@@ -39,7 +42,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
openConfig(`${message.guildId}-config.json`, 'toggle-chat', true) openConfig(`${message.guildId}-config.json`, 'toggle-chat', true)
reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...')) reject(new Error('Failed to locate or create Server Preferences\n\nPlease try chatting again...'))
} }
// check if chat is disabled // check if chat is disabled
else if (!config.options['toggle-chat']) else if (!config.options['toggle-chat'])
reject(new Error('Admin(s) have disabled chat features.\n\n Please contact your server\'s admin(s).')) reject(new Error('Admin(s) have disabled chat features.\n\n Please contact your server\'s admin(s).'))
@@ -48,12 +51,12 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
}) })
}) })
break // successful break // successful
} catch (error) { } catch (error) {
++attempt ++attempt
if (attempt < maxRetries) { if (attempt < maxRetries) {
log(`Attempt ${attempt} failed for Server Preferences. Retrying in ${delay}ms...`) log(`Attempt ${attempt} failed for Server Preferences. Retrying in ${delay}ms...`)
await new Promise(ret => setTimeout(ret, delay)) await new Promise(ret => setTimeout(ret, delay))
} else } else
throw new Error(`Could not retrieve Server Preferences, please try chatting again...`) throw new Error(`Could not retrieve Server Preferences, please try chatting again...`)
} }
} }
@@ -68,12 +71,11 @@ 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 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 return
} }
// check if there is a set capacity in config // check if there is a set capacity in config
else if (typeof config.options['modify-capacity'] !== 'number') else if (typeof config.options['modify-capacity'] !== 'number')
log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`) log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`)
@@ -89,18 +91,18 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
if (typeof config.options['switch-model'] !== 'string') if (typeof config.options['switch-model'] !== 'string')
reject(new Error(`No Model was set. Please set a model by running \`/switch-model <model of choice>\`.\n\nIf you do not have any models. Run \`/pull-model <model name>\`.`)) reject(new Error(`No Model was set. Please set a model by running \`/switch-model <model of choice>\`.\n\nIf you do not have any models. Run \`/pull-model <model name>\`.`))
resolve(config) resolve(config)
}) })
}) })
break // successful break // successful
} catch (error) { } catch (error) {
++attempt ++attempt
if (attempt < maxRetries) { if (attempt < maxRetries) {
log(`Attempt ${attempt} failed for User Preferences. Retrying in ${delay}ms...`) log(`Attempt ${attempt} failed for User Preferences. Retrying in ${delay}ms...`)
await new Promise(ret => setTimeout(ret, delay)) await new Promise(ret => setTimeout(ret, delay))
} else } else
throw new Error(`Could not retrieve User Preferences, please try chatting again...`) throw new Error(`Could not retrieve User Preferences, please try chatting again...`)
} }
} }
@@ -119,7 +121,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
if (chatMessages.length === 0) { if (chatMessages.length === 0) {
chatMessages = await new Promise((resolve, reject) => { chatMessages = await new Promise((resolve, reject) => {
openChannelInfo(message.channelId, openChannelInfo(message.channelId,
message.channel as TextChannel, message.channel as TextChannel,
message.author.tag message.author.tag
) )
@@ -134,14 +136,18 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
}) })
} }
// response string for ollama to put its response
let response: string
if (!userConfig) 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.`) 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 // get message attachment if exists
const messageAttachment: string[] = await getAttachmentData(message.attachments.first()) 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'] const model: string = userConfig.options['switch-model']
// set up new queue // set up new queue
@@ -156,12 +162,9 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
content: cleanedMessage, content: cleanedMessage,
images: messageAttachment || [] images: messageAttachment || []
}) })
// undefined or false, use normal, otherwise use embed // response string for ollama to put its response
if (userConfig.options['message-style']) const response: string = await normalMessage(message, ollama, model, msgHist, shouldStream)
response = await embedMessage(message, ollama, model, msgHist, shouldStream)
else
response = await normalMessage(message, ollama, model, msgHist, shouldStream)
// If something bad happened, remove user query and stop // If something bad happened, remove user query and stop
if (response == undefined) { msgHist.pop(); return } if (response == undefined) { msgHist.pop(); return }
@@ -170,16 +173,16 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
while (msgHist.size() >= msgHist.capacity) msgHist.dequeue() while (msgHist.size() >= msgHist.capacity) msgHist.dequeue()
// successful query, save it in context history // successful query, save it in context history
msgHist.enqueue({ msgHist.enqueue({
role: 'assistant', role: 'assistant',
content: response, content: response,
images: messageAttachment || [] images: messageAttachment || []
}) })
// only update the json on success // only update the json on success
openChannelInfo(message.channelId, openChannelInfo(message.channelId,
message.channel as TextChannel, message.channel as TextChannel,
message.author.tag, message.author.tag,
msgHist.getItems() msgHist.getItems()
) )
} catch (error: any) { } catch (error: any) {

View File

@@ -20,8 +20,8 @@ export default event(Events.ThreadDelete, async ({ log }, thread: ThreadChannel)
// filter files by thread id being deleted // filter files by thread id being deleted
const filesToDiscard = files.filter( const filesToDiscard = files.filter(
file => file.startsWith(`${thread.id}-`) && file => file.startsWith(`${thread.id}-`) &&
file.endsWith('.json')) file.endsWith('.json'))
// remove files by unlinking // remove files by unlinking
filesToDiscard.forEach(file => { filesToDiscard.forEach(file => {
@@ -36,5 +36,5 @@ export default event(Events.ThreadDelete, async ({ log }, thread: ThreadChannel)
}) })
} catch (error) { } catch (error) {
log(`Issue deleting user history files from ${thread.id}`) log(`Issue deleting user history files from ${thread.id}`)
} }
}) })

View File

@@ -1,10 +1,10 @@
import { getEnvVar } from './utils/index.js' import { getEnvVar } from './utils/index.js'
export const Keys = { 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')
} as const // readonly keys } as const // readonly keys
export default 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 * interface for how slash commands should be run
@@ -6,7 +6,7 @@ import { CommandInteraction, ChatInputApplicationCommandData, Client, Applicatio
export interface SlashCommand extends ChatInputApplicationCommandData { export interface SlashCommand extends ChatInputApplicationCommandData {
run: ( run: (
client: Client, client: Client,
interaction: CommandInteraction, interaction: ChatInputCommandInteraction,
options?: ApplicationCommandOption[] options?: ApplicationCommandOption[]
) => void ) => void
} }

View File

@@ -3,7 +3,6 @@ import { UserMessage } from './index.js'
export interface UserConfiguration { export interface UserConfiguration {
'message-stream'?: boolean, 'message-stream'?: boolean,
'message-style'?: boolean,
'modify-capacity': number, 'modify-capacity': number,
'switch-model': string 'switch-model': string
} }

View File

@@ -21,14 +21,13 @@ export function getEnvVar(name: string, fallback?: string): string {
throw new Error(`Environment variable ${name} is not set.`) throw new Error(`Environment variable ${name} is not set.`)
// validate User-Generated Discord Application Tokens // validate User-Generated Discord Application Tokens
if (name === "CLIENT_TOKEN") if (name === "CLIENT_TOKEN" && value.length > 72)
if (value.length < 72) throw new Error(`The "CLIENT_TOKEN" provided is not of at least length 72. throw new Error(`The "CLIENT_TOKEN" provided is not of at least length 72.
This is probably an invalid token unless Discord updated their token policy. Please provide a valid token.`) This is probably an invalid token unless Discord updated their token policy. Please provide a valid token.`)
// validate IPv4 address found in environment variables // validate IPv4 address found in environment variables
if (name.endsWith("_IP") || name.endsWith("_ADDRESS")) if ((name.endsWith("_IP") || name.endsWith("_ADDRESS")) && !ipValidate.test(value))
if (!ipValidate.test(value)) throw new Error(`Environment variable ${name} does not follow IPv4 formatting.`)
throw new Error(`Environment variable ${name} does not follow IPv4 formatting.`)
// return env variable // return env variable
return value return value

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'
@@ -15,8 +15,8 @@ export type EventKeys = keyof ClientEvents // only wants keys of ClientEvents ob
* @param msgHist message history * @param msgHist message history
*/ */
export type ChatParams = { export type ChatParams = {
model: string, model: string,
ollama: Ollama, ollama: Ollama,
msgHist: UserMessage[] msgHist: UserMessage[]
} }
@@ -24,6 +24,7 @@ export type ChatParams = {
* Format for the messages to be stored when communicating when the bot * Format for the messages to be stored when communicating when the bot
* @param role either assistant, user, or system * @param role either assistant, user, or system
* @param content string of the message the user or assistant provided * @param content string of the message the user or assistant provided
* @param images array of images that the user or assistant provided
*/ */
export type UserMessage = { export type UserMessage = {
role: string, role: string,
@@ -33,12 +34,18 @@ export type UserMessage = {
// Event properties // Event properties
export interface EventProps { export interface EventProps {
client: Client client: Client,
log: LogMethod log: LogMethod,
msgHist: Queue<UserMessage> msgHist: Queue<UserMessage>,
ollama: Ollama, ollama: Ollama,
defaultModel: String defaultModel: String
} }
/**
* Format for the callback function tied to an event
* @param props the properties of the event
* @param args the arguments of the event
*/
export type EventCallback<T extends EventKeys> = ( export type EventCallback<T extends EventKeys> = (
props: EventProps, props: EventProps,
...args: ClientEvents[T] ...args: ClientEvents[T]
@@ -50,6 +57,12 @@ export interface Event<T extends EventKeys = EventKeys> {
callback: EventCallback<T> callback: EventCallback<T>
} }
/**
* Method to create an event object
* @param key type of event
* @param callback function to run when event is triggered
* @returns event object
*/
export function event<T extends EventKeys>(key: T, callback: EventCallback<T>): Event<T> { export function event<T extends EventKeys>(key: T, callback: EventCallback<T>): Event<T> {
return { key, callback } return { key, callback }
} }
@@ -62,8 +75,8 @@ export function event<T extends EventKeys>(key: T, callback: EventCallback<T>):
* @param ollama the initialized ollama instance * @param ollama the initialized ollama instance
*/ */
export function registerEvents( export function registerEvents(
client: Client, client: Client,
events: Event[], events: Event[],
msgHist: Queue<UserMessage>, msgHist: Queue<UserMessage>,
ollama: Ollama, ollama: Ollama,
defaultModel: String defaultModel: String

View File

@@ -54,4 +54,13 @@ export async function getAttachmentData(attachment: Attachment | undefined): Pro
const buffer = await getAttachmentBuffer(url) const buffer = await getAttachmentBuffer(url)
const base64String = arrayBufferToBase64(buffer) const base64String = arrayBufferToBase64(buffer)
return [base64String] return [base64String]
}
/**
* Method to retrieve the string data from the text file
*
* @param attachment the text file to convert to a string
*/
export async function getTextFileAttachmentData(attachment: Attachment): Promise<string> {
return await (await fetch(attachment.url)).text()
} }

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) => {
@@ -80,7 +80,14 @@ export async function openChannelInfo(filename: string, channel: TextChannel | T
} }
}) })
} else { // file doesn't exist, create it } else { // file doesn't exist, create it
const object: Configuration = JSON.parse(`{ \"id\": \"${channel?.id}\", \"name\": \"${channel?.name}\", \"user\": \"${user}\", \"messages\": []}`) const object: Configuration = JSON.parse(
`{
\"id\": \"${channel?.id}\",
\"name\": \"${channel?.name}\",
\"user\": \"${user}\",
\"messages\": []
}`
)
const directory = path.dirname(fullFileName) const directory = path.dirname(fullFileName)
if (!fs.existsSync(directory)) if (!fs.existsSync(directory))
@@ -88,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`)
} }
} }
@@ -103,7 +110,7 @@ export async function getChannelInfo(filename: string, callback: (config: Channe
if (fs.existsSync(fullFileName)) { if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error) { if (error) {
callback(undefined) callback(undefined)
return // something went wrong... stop return // something went wrong... stop
} }
callback(JSON.parse(data)) callback(JSON.parse(data))

View File

@@ -10,9 +10,9 @@ 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
if (fs.existsSync(fullFileName)) { if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
@@ -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`)
} }
} }
@@ -58,7 +58,7 @@ export async function getServerConfig(filename: string, callback: (config: Serve
if (fs.existsSync(fullFileName)) { if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error) { if (error) {
callback(undefined) callback(undefined)
return // something went wrong... stop return // something went wrong... stop
} }
callback(JSON.parse(data)) callback(JSON.parse(data))
@@ -81,7 +81,7 @@ export async function getUserConfig(filename: string, callback: (config: UserCon
if (fs.existsSync(fullFileName)) { if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error) { if (error) {
callback(undefined) callback(undefined)
return // something went wrong... stop return // something went wrong... stop
} }
callback(JSON.parse(data)) callback(JSON.parse(data))

View File

@@ -1,11 +1,10 @@
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
* @param params * @param params
* @returns Asyn * @returns AsyncIterator<ChatResponse> generated by the Ollama client
*/ */
export async function streamResponse(params: ChatParams): Promise<AbortableAsyncIterator<ChatResponse>> { export async function streamResponse(params: ChatParams): Promise<AbortableAsyncIterator<ChatResponse>> {
return await params.ollama.chat({ return await params.ollama.chat({

View File

@@ -1,7 +1,6 @@
// Centralized import index // Centralized import index
export * from './env.js' export * from './env.js'
export * from './events.js' export * from './events.js'
export * from './messageEmbed.js'
export * from './messageNormal.js' export * from './messageNormal.js'
export * from './commands.js' export * from './commands.js'
export * from './configInterfaces.js' export * from './configInterfaces.js'

View File

@@ -1,5 +1,3 @@
import Keys from "../keys.js"
/** /**
* Clean up the bot user_id so it only has the prompt * Clean up the bot user_id so it only has the prompt
* *

View File

@@ -1,129 +0,0 @@
import { EmbedBuilder, Message, SendableChannels } from 'discord.js'
import { ChatResponse, Ollama } from 'ollama'
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
import { Queue } from '../queues/queue.js'
import { AbortableAsyncIterator } from 'ollama/src/utils.js'
/**
* Method to send replies as normal text on discord like any other user
* @param message message sent by the user
* @param model name of model to run query
* @param msgHist message history between user and model
*/
export async function embedMessage(
message: Message,
ollama: Ollama,
model: string,
msgHist: Queue<UserMessage>,
stream: boolean
): Promise<string> {
// bot response
let response: ChatResponse | AbortableAsyncIterator<ChatResponse>
let result: string = ''
// initial message to client
const botMessage = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription('Generating Response . . .')
.setColor('#00FF00')
// send the message
const channel = message.channel as SendableChannels
const sentMessage = await channel.send({ embeds: [botMessage] })
// create params
const params: ChatParams = {
model: model,
ollama: ollama,
msgHist: msgHist.getItems()
}
try {
// check if embed needs to stream
if (stream) {
response = await streamResponse(params)
for await (const portion of response) {
result += portion.message.content
// exceeds handled length
if (result.length > 5000) {
const errorEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(`Response length ${result.length} has exceeded Discord maximum.\n\nLong Stream messages not supported.`)
.setColor('#00FF00')
// send error
channel.send({ embeds: [errorEmbed] })
break // cancel loop and stop
}
// new embed per token...
const streamEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(result || 'No Content Yet...')
.setColor('#00FF00')
// edit the message
sentMessage.edit({ embeds: [streamEmbed] })
}
} else {
response = await blockResponse(params)
result = response.message.content
// long message, split into different embeds sadly.
if (result.length > 5000) {
const firstEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(result.slice(0, 5000) || 'No Content to Provide...')
.setColor('#00FF00')
// replace first embed
sentMessage.edit({ embeds: [firstEmbed] })
// take the rest out
result = result.slice(5000)
// handle the rest
while (result.length > 5000) {
const whileEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(result.slice(0, 5000) || 'No Content to Provide...')
.setColor('#00FF00')
channel.send({ embeds: [whileEmbed] })
result = result.slice(5000)
}
const lastEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(result || 'No Content to Provide...')
.setColor('#00FF00')
// rest of the response
channel.send({ embeds: [lastEmbed] })
} else {
// only need to create 1 embed, handles 6000 characters
const newEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(result || 'No Content to Provide...')
.setColor('#00FF00')
// edit the message
sentMessage.edit({ embeds: [newEmbed] })
}
}
} catch(error: any) {
console.log(`[Util: messageEmbed] Error creating message: ${error.message}`)
const errorEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(`**Response generation failed.**\n\nReason: ${error.message}`)
.setColor('#00FF00')
// send back error
sentMessage.edit({ embeds: [errorEmbed] })
}
// Hope there is a response! undefined otherwie
return result
}

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,
@@ -28,7 +28,7 @@ export async function normalMessage(
model: model, model: model,
ollama: ollama, ollama: ollama,
msgHist: msgHist.getItems() msgHist: msgHist.getItems()
} }
// run query based on stream preference, true = stream, false = block // run query based on stream preference, true = stream, false = block
if (stream) { if (stream) {
@@ -40,14 +40,15 @@ export async function normalMessage(
result = portion.message.content result = portion.message.content
// new message block, wait for it to send and assign new block to respond. // new message block, wait for it to send and assign new block to respond.
await channel.send("Creating new stream block...").then(sentMessage => { messageBlock = sentMessage }) await channel.send("Creating new stream block...")
.then(sentMessage => { messageBlock = sentMessage })
} else { } else {
result += portion.message.content result += portion.message.content
// ensure block is not empty // ensure block is not empty
if (result.length > 5) if (result.length > 5)
messageBlock.edit(result) messageBlock.edit(result)
} }
console.log(result) console.log(result)
} }
} }
@@ -70,9 +71,9 @@ export async function normalMessage(
channel.send(result) channel.send(result)
} else // edit the 'generic' response to new message since <2000 } else // edit the 'generic' response to new message since <2000
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('try pulling it first')) 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.`) 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 else

View File

@@ -1,72 +1,78 @@
// describe marks a test suite // describe marks a test suite
// expect takes a value from an expression // expect takes a value from an expression
// it marks a test case // it marks a test case
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import commands from '../src/commands/index.js' import commands from '../src/commands/index.js'
/** /**
* Commands test suite, tests the commands object * Mocking client.ts because of the commands
* Each command is to be tested elsewhere, this file */
* is to ensure that the commands object is defined. vi.mock('../src/client.js', () => ({}))
*
* @param name name of the test suite /**
* @param fn function holding tests to run * Commands test suite, tests the commands object
*/ * Each command is to be tested elsewhere, this file
describe('Commands Existence', () => { * is to ensure that the commands object is defined.
// test definition of commands object *
it('references defined object', () => { * @param name name of the test suite
// toBe compares the value to the expected value * @param fn function holding tests to run
expect(typeof commands).toBe('object') */
}) describe('Commands Existence', () => {
// test definition of commands object
// test specific commands in the object it('references defined object', () => {
it('references specific commands', () => { // toBe compares the value to the expected value
const commandsString = commands.map(e => e.name).join(', ') expect(typeof commands).toBe('object')
expect(commandsString).toBe('thread, private-thread, message-style, message-stream, toggle-chat, shutoff, modify-capacity, clear-user-channel-history, pull-model, switch-model') })
})
}) // test specific commands in the object
it('references specific commands', () => {
/** const commandsString = commands.map(e => e.name).join(', ')
* User Commands Test suite for testing out commands const expectedCommands = ['thread', 'private-thread', 'message-stream', 'toggle-chat', 'shutoff', 'modify-capacity', 'clear-user-channel-history', 'pull-model', 'switch-model', 'delete-model']
* that would be run by users when using the application. expect(commandsString).toBe(expectedCommands.join(', '))
*/ })
describe('User Command Tests', () => { })
// test capacity command
it('run modify-capacity command', () => { /**
* User Commands Test suite for testing out commands
}) * that would be run by users when using the application.
*/
it('run clear-user-channel-history command', () => { describe('User Command Tests', () => {
// test capacity command
}) it('run modify-capacity command', () => {
it('run message-stream command', () => { })
}) it('run clear-user-channel-history command', () => {
it('run message-style command', () => { })
}) it('run message-stream command', () => {
it('run thread command', () => { })
}) it('run message-style command', () => {
it('run private-thread command', () => { })
}) it('run thread command', () => {
})
})
/**
* Admin Commands Test suite for running administrative it('run private-thread command', () => {
* commands with the application.
*/ })
describe('Admin Command Tests', () => { })
it('run shutoff command', () => {
/**
}) * Admin Commands Test suite for running administrative
* commands with the application.
it('run toggle-chat command', () => { */
describe('Admin Command Tests', () => {
}) it('run shutoff command', () => {
})
it('run toggle-chat command', () => {
})
}) })

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
@@ -8,7 +8,7 @@ import { Queue } from '../src/queues/queue.js'
* @param fn function holding tests to run * @param fn function holding tests to run
*/ */
describe('Queue Structure', () => { describe('Queue Structure', () => {
let queue= new Queue<string>() let queue = new Queue<string>()
// test for queue creation // test for queue creation
it('creates a new queue', () => { it('creates a new queue', () => {