3 Commits

Author SHA1 Message Date
JT2M0L3Y
f632ae559f Update: fix imports based on last pkg fix 2025-06-20 15:22:43 -07:00
JT2M0L3Y
e85aff01a7 Added: defined objects directory 2025-06-20 15:22:42 -07:00
JT2M0L3Y
186b428454 Update: utility method logs use method name 2025-06-20 15:22:42 -07:00
27 changed files with 541 additions and 510 deletions

View File

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

View File

@@ -8,10 +8,10 @@ assignees: ''
--- ---
## Issue ## Issue
A clear and concise description of what the problem/feature is. PLease describe it as best as possible. A clear and concise description of what the problem/feature is.
## Solution ## Solution
* Provide steps or ideas to how to implement or investigate this new feature. * Provide steps or ideals to how to implement or investigate this new feature.
## References ## References
* Provide additional context and external references here * Provide additional context and external references here

View File

@@ -1,75 +1,79 @@
name: Builds name: Builds
run-name: Validate Node and Docker Builds run-name: Validate Node and Docker Builds
on: on:
push: push:
branches: branches:
- master # runs after Pull Request is merged - master # runs after Pull Request is merged
jobs: jobs:
Discord-Node-Build: # test if the node install and run Discord-Node-Build: # test if the node install and run
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 2 timeout-minutes: 2
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/jod - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/jod node-version: lts/jod
cache: "npm" cache: "npm"
- name: Install Project Dependencies - name: Install Project Dependencies
run: | run: |
npm install npm install
- name: Build Application - name: Build Application
run: | run: |
npm run build npm run build
- name: Create Environment Variables - name: Create Environment Variables
run: | run: |
touch .env touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
# set -e ensures if nohup fails, this section fails echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Startup Discord Bot Client
run: | # set -e ensures if nohup fails, this section fails
set -e - name: Startup Discord Bot Client
nohup npm run prod & run: |
set -e
Discord-Ollama-Container-Build: # test docker build and run nohup npm run prod &
runs-on: ubuntu-latest
timeout-minutes: 2 Discord-Ollama-Container-Build: # test docker build and run
steps: runs-on: ubuntu-latest
- name: Checkout Repository timeout-minutes: 2
uses: actions/checkout@v4 steps:
- name: Checkout Repository
- name: Set up Node Environment lts/jod uses: actions/checkout@v4
uses: actions/setup-node@v4
with: - name: Set up Node Environment lts/jod
node-version: lts/jod uses: actions/setup-node@v4
cache: "npm" with:
node-version: lts/jod
- name: Create Environment Variables cache: "npm"
run: |
touch .env - name: Create Environment Variables
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env run: |
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env touch .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Setup Docker Network and Images echo MODEL = ${{ secrets.MODEL }} >> .env
run: | echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
npm run docker:start-cpu echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Check Images Exist - name: Setup Docker Network and Images
run: | run: |
(docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama') || exit 1 npm run docker:start-cpu
- name: Check Containers Exist - name: Check Images Exist
run: | run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord') || exit 1 (docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama' | docker images | grep -qE 'redis') || exit 1
- name: Check Containers Exist
run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord' && docker ps | grep -q 'redis') || exit 1

View File

@@ -31,6 +31,8 @@ jobs:
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Collect Code Coverage - name: Collect Code Coverage
run: | run: |

View File

@@ -1,10 +1,9 @@
name: Deploy name: Deploy
run-name: Deploy Application Latest run-name: Deploy Application Latest
# on: on:
# push: push:
# tags: tags:
# - 'v*' - 'v*'
on: workflow_dispatch
jobs: jobs:
Deploy-Application: Deploy-Application:
@@ -25,6 +24,8 @@ jobs:
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
echo DISCORD_IP = ${{ secrets.DISCORD_IP }} >> .env echo DISCORD_IP = ${{ secrets.DISCORD_IP }} >> .env
echo SUBNET_ADDRESS = ${{ secrets.SUBNET_ADDRESS }} >> .env echo SUBNET_ADDRESS = ${{ secrets.SUBNET_ADDRESS }} >> .env
echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
- name: Check if directory exists and delete it - name: Check if directory exists and delete it
run: | run: |
@@ -58,6 +59,7 @@ jobs:
npm install npm install
IMAGE="kevinthedang/discord-ollama" IMAGE="kevinthedang/discord-ollama"
REDIS="redis"
OLLAMA="ollama/ollama" OLLAMA="ollama/ollama"
if docker images | grep -q $IMAGE; then if docker images | grep -q $IMAGE; then
@@ -73,6 +75,19 @@ jobs:
echo "Old $IMAGE Image Removed" echo "Old $IMAGE Image Removed"
fi fi
if docker images | grep -q $REDIS; then
IMAGE_ID=$(docker images -q $REDIS)
CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID")
if [ ! -z "$CONTAINER_IDS" ]; then
# Stop and remove the running containers
docker stop $CONTAINER_IDS
echo "Stopped and removed the containers using the image $REDIS"
fi
docker rmi $IMAGE_ID
echo "Old $REDIS Image Removed"
fi
if docker images | grep -q $OLLAMA; then if docker images | grep -q $OLLAMA; then
IMAGE_ID=$(docker images -q $OLLAMA) IMAGE_ID=$(docker images -q $OLLAMA)
CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID") CONTAINER_IDS=$(docker ps -q --filter "ancestor=$IMAGE_ID")
@@ -102,6 +117,14 @@ jobs:
--ip ${{ secrets.OLLAMA_IP }} \ --ip ${{ secrets.OLLAMA_IP }} \
ollama/ollama:latest ollama/ollama:latest
docker run --rm -d \
-v redis:/root/.redis \
-p ${{ secrets.REDIS_PORT }}:${{ secrets.REDIS_PORT }} \
--name redis \
--network ollama-net \
--ip ${{ secrets.REDIS_IP }} \
redis:latest
docker run --rm -d \ docker run --rm -d \
-v discord:/src/app \ -v discord:/src/app \
--name discord \ --name discord \

View File

@@ -1,48 +1,50 @@
name: Tests name: Tests
run-name: Unit Tests run-name: Unit Tests
on: on:
pull_request: pull_request:
branches: branches:
- master - master
paths: paths:
- '*' - '*'
- 'package*.json' - 'package*.json'
- 'src/**' - 'src/**'
- 'tests/**' - 'tests/**'
- '!docs/**' - '!docs/**'
- '!imgs/**' - '!imgs/**'
- '!.github/**' - '!.github/**'
- '.github/workflows/**' - '.github/workflows/**'
- '!.gitignore' - '!.gitignore'
- '!LICENSE' - '!LICENSE'
- '!README' - '!README'
jobs: jobs:
Discord-Node-Test: Discord-Node-Test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 2 timeout-minutes: 2
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/jod - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/jod node-version: lts/jod
cache: "npm" cache: "npm"
- name: Install Project Dependencies - name: Install Project Dependencies
run: | run: |
npm install npm install
- name: Create Environment Variables - name: Create Environment Variables
run: | run: |
touch .env touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
echo REDIS_IP = ${{ secrets.REDIS_IP }} >> .env
- name: Test Application echo REDIS_PORT = ${{ secrets.REDIS_PORT }} >> .env
run: |
npm run tests - name: Test Application
run: |
npm run tests

View File

@@ -4,9 +4,9 @@
<h3><a href="#"></a>Ollama as your Discord AI Assistant</h3> <h3><a href="#"></a>Ollama as your Discord AI Assistant</h3>
<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="Builds" 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/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/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="Tests" 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,28 +14,19 @@
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 and Server Preferences * [x] User Preferences on Chat
* [x] Message Persistance * [x] Message Persistance on Channels and Threads
* [x] Threads
* [x] Channels
* [x] Containerization with Docker * [x] Containerization with Docker
* [x] Slash Commands Compatible * [x] Slash Commands Compatible
* [x] 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] Multi-User Chat Generation - This was built in from Ollama `v0.2.1+` * [x] User vs. Server Preferences
* [ ] Ollama Tool Support Implementation * [ ] Redis Caching
* [ ] Enhanced Channel Context Awareness * [x] Administrator Role Compatible
* [ ] Improved User Replied Triggers * [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
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. 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) * [Customize a model](https://github.com/ollama/ollama#customize-a-model)
@@ -63,6 +54,8 @@ These are guides to the features and capabilities of this app.
* This project requires the use of npm version `10.9.0` or above. * This project requires the use of npm version `10.9.0` or above.
* [Ollama](https://ollama.com/) * [Ollama](https://ollama.com/)
* [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama) * [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama)
* [Redis](https://redis.io/)
* [Redis Docker Image](https://hub.docker.com/_/redis)
* [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main) * [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main)
* [Setting up Docker (Ubuntu 20.04)](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04) * [Setting up Docker (Ubuntu 20.04)](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
* [Setting up Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) * [Setting up Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)

View File

@@ -7,12 +7,14 @@ 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.9.0 image: kevinthedang/discord-ollama:0.8.5
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} MODEL: ${MODEL}
REDIS_IP: ${REDIS_IP}
REDIS_PORT: ${REDIS_PORT}
networks: networks:
ollama-net: ollama-net:
ipv4_address: ${DISCORD_IP} ipv4_address: ${DISCORD_IP}
@@ -35,6 +37,19 @@ services:
ports: ports:
- ${OLLAMA_PORT}:${OLLAMA_PORT} - ${OLLAMA_PORT}:${OLLAMA_PORT}
# setup redis container
redis:
image: redis:latest
container_name: redis
restart: always
networks:
ollama-net:
ipv4_address: ${REDIS_IP}
volumes:
- redis:/root/.redis
ports:
- ${REDIS_PORT}:${REDIS_PORT}
# create a network that supports giving addresses withing a specific subnet # create a network that supports giving addresses withing a specific subnet
networks: networks:
ollama-net: ollama-net:
@@ -47,3 +62,4 @@ networks:
volumes: volumes:
ollama: ollama:
discord: discord:
redis:

View File

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

107
package-lock.json generated
View File

@@ -1,17 +1,18 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.9.0", "version": "0.8.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.9.0", "version": "0.8.5",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.20.0", "discord.js": "^14.20.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ollama": "^0.5.15" "ollama": "^0.5.15",
"redis": "^4.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
@@ -768,6 +769,65 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz",
"integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz",
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz",
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz",
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.37.0", "version": "4.37.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz",
@@ -1413,6 +1473,15 @@
"node": ">= 16" "node": ">= 16"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1661,6 +1730,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/get-tsconfig": { "node_modules/get-tsconfig": {
"version": "4.10.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
@@ -2024,6 +2102,23 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/redis": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz",
"integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==",
"license": "MIT",
"workspaces": [
"./packages/*"
],
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.6.0",
"@redis/graph": "1.1.1",
"@redis/json": "1.0.7",
"@redis/search": "1.2.0",
"@redis/time-series": "1.1.0"
}
},
"node_modules/resolve-pkg-maps": { "node_modules/resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -2772,6 +2867,12 @@
} }
} }
}, },
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yn": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.9.0", "version": "0.8.5",
"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",
@@ -13,22 +13,24 @@
"client": "npm run build && npm run prod", "client": "npm run build && npm run prod",
"clean": "docker compose down && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)", "clean": "docker compose down && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"start": "docker compose build --no-cache && docker compose up -d", "start": "docker compose build --no-cache && docker compose up -d",
"docker:clean": "docker rm -f discord && docker rm -f ollama && docker network prune -f && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)", "docker:clean": "docker rm -f discord && docker rm -f ollama && docker rm -f redis && docker network prune -f && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
"docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net", "docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net",
"docker:build": "docker build --no-cache -t kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\") .", "docker:build": "docker build --no-cache -t kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\") .",
"docker:build-latest": "docker build --no-cache -t kevinthedang/discord-ollama:latest .", "docker:build-latest": "docker build --no-cache -t kevinthedang/discord-ollama:latest .",
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\")", "docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 kevinthedang/discord-ollama:$(node -p \"require('./package.json').version\")",
"docker:redis": "docker run -d -v redis:/root/.redis -p 6379:6379 --name redis --network ollama-net --ip 172.18.0.4 redis:latest",
"docker:ollama": "docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest", "docker:ollama": "docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest",
"docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest", "docker:ollama-cpu": "docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama --network ollama-net --ip 172.18.0.2 ollama/ollama:latest",
"docker:start": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama", "docker:start": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:redis && npm run docker:client && npm run docker:ollama",
"docker:start-cpu": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama-cpu" "docker:start-cpu": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:redis && npm run docker:client && npm run docker:ollama-cpu"
}, },
"author": "Kevin Dang", "author": "Kevin Dang",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.20.0", "discord.js": "^14.20.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ollama": "^0.5.15" "ollama": "^0.5.15",
"redis": "^4.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
@@ -43,4 +45,4 @@
"npm": ">=10.9.0", "npm": ">=10.9.0",
"node": ">=22.12.0" "node": ">=22.12.0"
} }
} }

View File

@@ -1,44 +1,59 @@
import { Client, GatewayIntentBits } from 'discord.js' import { Client, GatewayIntentBits } from 'discord.js'
import { Ollama } from 'ollama' import { Ollama } from 'ollama'
import { Queue } from './queues/queue.js' import { createClient } from 'redis'
import { UserMessage, registerEvents } from './utils/index.js' import { Queue } from './components/index.js'
import Events from './events/index.js' import { UserMessage, registerEvents } from './utils/index.js'
import Keys from './keys.js' import Events from './events/index.js'
import Keys from './keys.js'
// initialize the client with the following permissions when logging in
const client = new Client({ // initialize the client with the following permissions when logging in
intents: [ const client = new Client({
GatewayIntentBits.Guilds, intents: [
GatewayIntentBits.GuildMembers, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent GatewayIntentBits.GuildMessages,
] GatewayIntentBits.MessageContent
}) ]
})
// initialize connection to ollama container
export const ollama = new Ollama({ // initialize connection to redis
host: `http://${Keys.ipAddress}:${Keys.portAddress}`, const redis = createClient({
}) url: `redis://${Keys.redisHost}:${Keys.redisPort}`,
})
// Create Queue managed by Events
const messageHistory: Queue<UserMessage> = new Queue<UserMessage> // initialize connection to ollama container
export const ollama = new Ollama({
// Create Channel History Queue managed by Events host: `http://${Keys.ipAddress}:${Keys.portAddress}`,
const channelMessageHistory: Queue<UserMessage> = new Queue<UserMessage> })
// register all events // Create Queue managed by Events
registerEvents(client, Events, messageHistory, channelMessageHistory, ollama, Keys.defaultModel) const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
// Try to log in the client // register all events
await client.login(Keys.clientToken) registerEvents(client, Events, messageHistory, ollama, Keys.defaultModel)
.catch((error) => {
console.error('[Login Error]', error) // Try to connect to redis
process.exit(1) await redis.connect()
}) .then(response => {
console.log('[Redis] Successfully Connected')
// queue up bots name })
messageHistory.enqueue({ .catch(error => {
role: 'assistant', console.error('[Redis] Connection Error. See error below:\n', error)
content: `My name is ${client.user?.username}`, console.warn('[Redis] Failed to connect to Redis Database, using local system')
images: [] // TODO: create boolean flag that will probably be used in messageCreate.ts if redis database is down
// When implementing this boolean flag, move connection to database BEFORE the registerEvents method
})
// Try to log in the client
await client.login(Keys.clientToken)
.catch((error) => {
console.error('[Login Error]', error)
process.exit(1)
})
// queue up bots name
messageHistory.enqueue({
role: 'assistant',
content: `My name is ${client.user?.username}`,
images: []
}) })

View File

@@ -9,7 +9,6 @@ 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' import { DeleteModel } from './deleteModel.js'
import { Summary } from './summary.js'
export default [ export default [
ThreadCreate, ThreadCreate,
@@ -21,6 +20,5 @@ export default [
ClearUserChannelHistory, ClearUserChannelHistory,
PullModel, PullModel,
SwitchModel, SwitchModel,
DeleteModel, DeleteModel
Summary
] as SlashCommand[] ] as SlashCommand[]

View File

@@ -1,50 +0,0 @@
import { Client, CommandInteraction, Message, MessageFlags, SendableChannels } from "discord.js";
import { accessChannelContext, normalMessage, SlashCommand, summarizeContextHistory, UserCommand, UserMessage } from "../utils/index.js";
import { ollama } from "../client.js"
export const Summary: SlashCommand = {
name: 'summary',
description: 'provides a summary of the chat history.',
// Generate Summary from additional context
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || !UserCommand.includes(channel.type)) return
// Defer
await interaction.deferReply({ flags: MessageFlags.Ephemeral })
// create summary using context
let channelContext: UserMessage[] = await new Promise((resolve) => {
accessChannelContext(interaction.channelId, (additionalContext) => {
if (additionalContext?.messages)
resolve(additionalContext.messages)
else
resolve([])
})
})
if (channelContext.length === 0) {
interaction.reply({
content: `There are no recent chat messages in this channel.`,
flags: MessageFlags.Ephemeral
})
return
}
// Push summarize prompt
channelContext.push({
role: "user",
content: "Please Summarize everything from the prior messages, provide in bullet point fashion on what people have said prior. For example: \"Someone mentioned/talked about ...\"",
images: []
})
// todo: instead of a default of codellama, we can user default model somehow. Look into later.
const response: string = await summarizeContextHistory(ollama, "codellama", channelContext)
console.log(response)
interaction.editReply({ content: response })
}
}

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

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

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

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

View File

@@ -1,6 +1,6 @@
import { TextChannel } from 'discord.js' import { TextChannel } from 'discord.js'
import { event, Events, normalMessage, UserMessage, clean, addToChannelContext } from '../utils/index.js'
import { import {
event, Events, normalMessage, UserMessage, clean,
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
} from '../utils/index.js' } from '../utils/index.js'
@@ -11,7 +11,7 @@ import {
* *
* @param message the message received from the channel * @param message the message received from the channel
*/ */
export default event(Events.MessageCreate, async ({ log, msgHist, channelHistory, 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
let 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}.`)
@@ -19,61 +19,6 @@ export default event(Events.MessageCreate, async ({ log, msgHist, channelHistory
// Do not respond if bot talks in the chat // Do not respond if bot talks in the chat
if (message.author.username === message.client.user.username) return 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 // Only respond if message mentions the bot
if (!message.mentions.has(clientId)) return if (!message.mentions.has(clientId)) return
@@ -194,6 +139,15 @@ export default event(Events.MessageCreate, async ({ log, msgHist, channelHistory
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
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

View File

@@ -1 +1 @@
import('./client.js') import('./client.js')

View File

@@ -1,10 +1,12 @@
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 redisHost: getEnvVar('REDIS_IP', '172.18.0.4'), // default redis host if none
redisPort: parseInt(getEnvVar('REDIS_PORT', '6379')) // default redis port if none
} as const // readonly keys
export default Keys export default Keys

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

View File

@@ -56,54 +56,6 @@ export async function clearChannelInfo(filename: string, channel: TextChannel, u
return cleanedHistory 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`)
}
}
export async function accessChannelContext(filename: string, callback: (config: Channel | undefined) => void): Promise<void> {
const fullFileName = `data/${filename}-context.json`
if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error) {
callback(undefined)
return // something went wrong... stop
}
callback(JSON.parse(data))
})
} else {
callback(undefined) // file not found
}
}
/** /**
* Method to open the channel history * Method to open the channel history
* *
@@ -112,7 +64,7 @@ export async function accessChannelContext(filename: string, callback: (config:
* @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) => {
@@ -143,7 +95,7 @@ export async function openChannelInfo(filename: string, channel: TextChannel | T
// only creating it, no need to add anything // only creating it, no need to add anything
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2)) fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
console.log(`[Util: openChannelInfo] Created '${fullFileName}' in working directory`) console.log(`[Util: ${this.name}] Created '${fullFileName}' in working directory`)
} }
} }

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import { Message, SendableChannels } from 'discord.js' import { Message, SendableChannels } from 'discord.js'
import { ChatResponse, Ollama } from 'ollama' import { ChatResponse, Ollama, AbortableAsyncIterator } from 'ollama'
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js' import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
import { Queue } from '../queues/queue.js' import { Queue } from '../components/index.js'
import { AbortableAsyncIterator } from 'ollama/src/utils.js'
/** /**
* Method to send replies as normal text on discord like any other user * Method to send replies as normal text on discord like any other user
@@ -11,6 +10,7 @@ import { AbortableAsyncIterator } from 'ollama/src/utils.js'
* @param msgHist message history between user and model * @param msgHist message history between user and model
*/ */
export async function normalMessage( export async function normalMessage(
this: any,
message: Message, message: Message,
ollama: Ollama, ollama: Ollama,
model: string, model: string,
@@ -56,10 +56,6 @@ export async function normalMessage(
response = await blockResponse(params) response = await blockResponse(params)
result = response.message.content 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 // check if message length > discord max for normal messages
if (result.length > 2000) { if (result.length > 2000) {
sentMessage.edit(result.slice(0, 2000)) sentMessage.edit(result.slice(0, 2000))
@@ -77,51 +73,14 @@ export async function normalMessage(
sentMessage.edit(result) sentMessage.edit(result)
} }
} catch (error: any) { } catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${error.message}`) console.log(`[Util: ${this.name}] Error creating message: ${error.message}`)
if (error.message.includes('fetch failed')) if (error.message.includes('try pulling it first'))
error.message = 'Missing ollama service on machine' sentMessage.edit(`**Response generation failed.**\n\nReason: You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.`)
else if (error.message.includes('try pulling it first')) else
error.message = `You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.` sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
} }
}) })
// return the string representation of ollama query response // return the string representation of ollama query response
return result return result
} }
export async function summarizeContextHistory(
ollama: Ollama,
model: string,
msgHist: UserMessage[],
): Promise<string> {
// todo: this stuff to create a response in a simpler way!
try {
const params: ChatParams = {
model: model,
ollama: ollama,
msgHist: msgHist
}
const response: ChatResponse = await blockResponse(params)
let result = response.message.content
if (hasThinking(result))
result = result.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
return result
} catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${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.`
return `**Response generation failed.**\n\nReason: ${error.message}`
}
}
// Region: Helpers
function hasThinking(message: string): boolean {
return /<think>[\s\S]*?<\/think>/i.test(message)
}
// End Region: Helpers

View File

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