Compare 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
31 changed files with 2223 additions and 1842 deletions
+4 -6
View File
@@ -4,13 +4,11 @@ CLIENT_TOKEN = BOT_TOKEN
# Default model for new users # Default model for new users
MODEL = DEFAULT_MODEL MODEL = DEFAULT_MODEL
# ip/port address of the LLM server (ollama, llmman, ...), I use 172.18.0.3 for docker, 127.0.0.1 for local # ip/port address of docker container, I use 172.18.0.3 for docker, 127.0.0.1 for local
# ollama listens on 11434, llmman (https://github.com/llmmanorg/llmman) on 17434 OLLAMA_IP = IP_ADDRESS
# OLLAMA_IP / OLLAMA_PORT are still accepted as deprecated aliases OLLAMA_PORT = PORT
LLM_ENDPOINT = IP_ADDRESS
LLM_PORT = PORT
# ip address for discord bot container, I use 172.18.0.2, use different IP than llm_endpoint # ip address for discord bot container, I use 172.18.0.2, use different IP than ollama_ip
DISCORD_IP = IP_ADDRESS 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.
+2 -2
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
+11 -13
View File
@@ -1,6 +1,5 @@
name: Builds name: Builds
run-name: Validate Node and Docker Builds run-name: Validate Node and Docker Builds
on: on:
push: push:
branches: branches:
@@ -10,16 +9,15 @@ 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 - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/krypton node-version: lts/jod
cache: npm cache: "npm"
- name: Install Project Dependencies - name: Install Project Dependencies
run: | run: |
@@ -33,8 +31,8 @@ jobs:
run: | run: |
touch .env touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo LLM_ENDPOINT = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo LLM_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
# set -e ensures if nohup fails, this section fails # set -e ensures if nohup fails, this section fails
@@ -45,23 +43,23 @@ jobs:
Discord-Ollama-Container-Build: # test docker build and run Discord-Ollama-Container-Build: # test docker build and run
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 3 timeout-minutes: 2
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment - name: Set up Node Environment lts/jod
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/krypton node-version: lts/jod
cache: npm cache: "npm"
- 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 LLM_ENDPOINT = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo LLM_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
- name: Setup Docker Network and Images - name: Setup Docker Network and Images
+50
View File
@@ -0,0 +1,50 @@
name: Coverage
run-name: Code Coverage
on:
push:
branches:
- master
jobs:
Discord-Node-Coverage:
runs-on: ubuntu-latest
environment: coverage
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment lts/jod
uses: actions/setup-node@v4
with:
node-version: lts/jod
cache: "npm"
- name: Install Project Dependencies
run: |
npm install
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
- name: Collect Code Coverage
run: |
LINE_PCT=$(npm run coverage | tail -2 | head -1 | awk '{print $3}')
echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV
- name: Upload Code Coverage
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: ${{ vars.GIST_ID }}
filename: coverage.json
label: Coverage
message: ${{ env.COVERAGE }}
valColorRange: ${{ env.COVERAGE }}
maxColorRange: 100
minColorRange: 0
+7 -6
View File
@@ -1,14 +1,15 @@
name: Deploy name: Deploy
run-name: Deploy Application Latest run-name: Deploy Application Latest
on:
on: workflow_dispatch push:
tags:
- 'v*'
jobs: jobs:
Deploy-Application: Deploy-Application:
runs-on: self-hosted runs-on: self-hosted
environment: deploy environment: deploy
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -18,8 +19,8 @@ jobs:
run: | run: |
touch .env touch .env
echo CLIENT_TOKEN = ${{ secrets.CLIENT }} >> .env echo CLIENT_TOKEN = ${{ secrets.CLIENT }} >> .env
echo LLM_ENDPOINT = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo LLM_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
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
@@ -38,7 +39,7 @@ jobs:
git clone https://github.com/kevinthedang/discord-ollama.git ${{ secrets.PATH }} git clone https://github.com/kevinthedang/discord-ollama.git ${{ secrets.PATH }}
cd ${{ secrets.PATH }} cd ${{ secrets.PATH }}
- name: Install nvm and Node.js - name: Install nvm and Node.js lts/jod
run: | run: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
-47
View File
@@ -1,47 +0,0 @@
name: Tests (Fork PRs)
run-name: Unit Tests
on:
pull_request:
branches:
- master
paths:
- '*'
- 'package*.json'
- 'src/**'
- 'tests/**'
- '!docs/**'
- '!imgs/**'
- '!.github/**'
- '.github/workflows/**'
- '!.gitignore'
- '!LICENSE'
- '!README'
jobs:
Discord-Node-Test-Fork:
if: ${{ github.event.pull_request.head.repo.fork == true }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/jod'
cache: npm
- name: Install Project Dependencies
run: |
npm install
- name: Create Dummy Env
run: |
echo CLIENT_TOKEN=test-token >> .env
echo LLM_ENDPOINT=127.0.0.1 >> .env
echo LLM_PORT=11434 >> .env
echo MODEL=dummy-model >> .env
- name: Test Application
run: |
npm run tests
+4 -7
View File
@@ -1,6 +1,5 @@
name: Tests name: Tests
run-name: Unit Tests run-name: Unit Tests
on: on:
pull_request: pull_request:
branches: branches:
@@ -20,19 +19,17 @@ on:
jobs: jobs:
Discord-Node-Test: Discord-Node-Test:
if: ${{ github.event.pull_request.head.repo.fork == false }}
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 - 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: |
@@ -42,8 +39,8 @@ jobs:
run: | run: |
touch .env touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo LLM_ENDPOINT = ${{ secrets.OLLAMA_IP }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo LLM_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
- name: Test Application - name: Test Application
+10
View File
@@ -37,6 +37,16 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
+1 -1
View File
@@ -12,7 +12,7 @@
# Technical/Business Code Ownership # Technical/Business Code Ownership
/src/ @kevinthedang @JT2M0L3Y /src/ @kevinthedang @JT2M0L3Y
/tests/ @JT2M0L3Y /tests/ @JT2M0L3Y
/.github/ @kevinthedang @JT2M0L3Y /.github/ @kevinthedang
# Docker Ownership # Docker Ownership
Dockerfile @kevinthedang Dockerfile @kevinthedang
+2 -2
View File
@@ -1,5 +1,5 @@
# use node LTS image for version 24 # use node LTS image for version 22
FROM node:krypton-alpine FROM node:jod-alpine
# set working directory inside container # set working directory inside container
WORKDIR /app WORKDIR /app
+33 -12
View File
@@ -4,19 +4,42 @@
<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/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/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/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>
## About ## About/Goals
Discord Ollama is a Discord bot that lets users interact with locally hosted Ollama is an AI model management tool that allows users to install and use custom large language models locally.
Ollama language models directly from Discord. The project aims to:
* [x] Create a Discord bot that will utilize Ollama and chat to chat with users!
* [x] User and Server Preferences
* [x] Message Persistance
* [x] Containerization with Docker
* [x] Slash Commands Compatible
* [ ] Summary Command
* [ ] Model Info Command
* [ ] List Models Command
* [x] Pull Model Command
* [x] Switch Model Command
* [x] Delete Model Command
* [x] Create Thread Command
* [x] Create Private Thread Command
* [x] Message Stream Command
* [x] Change Message History Size Command
* [x] Clear Channel History Command (User Only)
* [x] Administrator Role Compatible
* [x] Generated Token Length Handling for >2000
* [x] Token Length Handling of any message size
* [x] Multi-User Chat Generation - This was built in from Ollama `v0.2.1+`
* [ ] Ollama Tool Support Implementation
* [ ] Enhanced Channel Context Awareness
* [ ] Improved User Replied Triggers
The bot supports model management, streamed responses, conversation history, 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.
private threads, and configurable server controls. Because Ollama runs locally, * [Customize a model](https://github.com/ollama/ollama#customize-a-model)
you retain control over the models and data used by the bot. * [Modelfile Docs](https://github.com/ollama/ollama/blob/main/docs/modelfile.md)
See the [setup guides](./docs/setup-local.md) to get started.
## Documentation ## Documentation
These are guides to the features and capabilities of this app. These are guides to the features and capabilities of this app.
@@ -27,7 +50,6 @@ These are guides to the features and capabilities of this app.
* Clone this repo using `git clone https://github.com/kevinthedang/discord-ollama.git` or just use [GitHub Desktop](https://desktop.github.com/) to clone the repo. * Clone this repo using `git clone https://github.com/kevinthedang/discord-ollama.git` or just use [GitHub Desktop](https://desktop.github.com/) to clone the repo.
* You will need a `.env` file in the root of the project directory with the bot's token. There is a `.env.sample` is provided for you as a reference for what environment variables. * You will need a `.env` file in the root of the project directory with the bot's token. There is a `.env.sample` is provided for you as a reference for what environment variables.
* For example, `CLIENT_TOKEN = [Bot Token]` * For example, `CLIENT_TOKEN = [Bot Token]`
* The LLM server is configured with `LLM_ENDPOINT` / `LLM_PORT` (`OLLAMA_IP` / `OLLAMA_PORT` still work as deprecated aliases).
* Please refer to the docs for bot setup. * Please refer to the docs for bot setup.
* [Creating a Discord App](./docs/setup-discord-app.md) * [Creating a Discord App](./docs/setup-discord-app.md)
* [Local Machine Setup](./docs/setup-local.md) * [Local Machine Setup](./docs/setup-local.md)
@@ -41,7 +63,6 @@ These are guides to the features and capabilities of this app.
* This project requires the use of npm version `10.9.0` or above. * 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)
* [llmman](https://github.com/llmmanorg/llmman) also serves the Ollama API (on port `17434`) and can be used in its place, see [Local Machine Setup](./docs/setup-local.md#using-with-llmman-alternative-to-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)
* [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)
+5 -5
View File
@@ -7,11 +7,11 @@ 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.2 image: kevinthedang/discord-ollama:0.8.6
environment: environment:
CLIENT_TOKEN: ${CLIENT_TOKEN} CLIENT_TOKEN: ${CLIENT_TOKEN}
LLM_ENDPOINT: ${LLM_ENDPOINT:-${OLLAMA_IP}} # OLLAMA_IP is a deprecated alias OLLAMA_IP: ${OLLAMA_IP}
LLM_PORT: ${LLM_PORT:-${OLLAMA_PORT}} # OLLAMA_PORT is a deprecated alias OLLAMA_PORT: ${OLLAMA_PORT}
MODEL: ${MODEL} MODEL: ${MODEL}
networks: networks:
ollama-net: ollama-net:
@@ -26,14 +26,14 @@ services:
restart: always restart: always
networks: networks:
ollama-net: ollama-net:
ipv4_address: ${LLM_ENDPOINT:-${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
volumes: volumes:
- ollama:/root/.ollama - ollama:/root/.ollama
ports: ports:
- ${LLM_PORT:-${OLLAMA_PORT}}:${LLM_PORT:-${OLLAMA_PORT}} - ${OLLAMA_PORT}:${OLLAMA_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:
+3 -3
View File
@@ -43,10 +43,10 @@ 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`, `LLM_ENDPOINT`, `LLM_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` * `SUBNET_ADDRESS = 172.18.0.0`
* `LLM_ENDPOINT = 172.18.0.2` * `OLLAMA_IP = 172.18.0.2`
* `LLM_PORT = 11434` * `OLLAMA_PORT = 11434`
* `DISCORD_IP = 172.18.0.3` * `DISCORD_IP = 172.18.0.3`
* 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)
+3 -10
View File
@@ -14,18 +14,11 @@
> [!NOTE] > [!NOTE]
> You can now pull models directly from the Discord client using `/pull-model <model-name>` or `/switch-model <model-name>`. They must exist from your local model library or from the [Ollama Model Library](https://ollama.com/library) > You can now pull models directly from the Discord client using `/pull-model <model-name>` or `/switch-model <model-name>`. They must exist from your local model library or from the [Ollama Model Library](https://ollama.com/library)
## Using with llmman (alternative to Ollama)
* [llmman](https://github.com/llmmanorg/llmman) is a local model runner that serves the Ollama API on port `17434`. The bot talks to it exactly as it would to Ollama, only the port differs.
* Install it with `curl -fsSL https://raw.githubusercontent.com/llmmanorg/llmman/main/install.sh | sh` (Linux/macOS) or `irm https://raw.githubusercontent.com/llmmanorg/llmman/main/install.ps1 | iex` (Windows).
* Start the server with `llmman serve` and pull a model with `llmman pull [model name]`, for example `llmman pull gemma4` or `llmman pull hf.co/unsloth/Qwen3.5-0.8B-GGUF`.
* Point the bot at it by setting `LLM_ENDPOINT = 127.0.0.1` and `LLM_PORT = 17434` in your `.env`, then set `MODEL` to a model you pulled. `/pull-model`, `/switch-model` and `/delete-model` work the same way.
## To Run Locally (without Docker) ## To Run Locally (without Docker)
* Run `npm install` to install the npm packages. * Run `npm install` to install the npm packages.
* Ensure that your [.env](../.env.sample) file's `LLM_ENDPOINT` is `127.0.0.1` to work properly. * Ensure that your [.env](../.env.sample) file's `OLLAMA_IP` is `127.0.0.1` to work properly.
* You only need your `CLIENT_TOKEN`, `LLM_ENDPOINT`, `LLM_PORT`. * You only need your `CLIENT_TOKEN`, `OLLAMA_IP`, `OLLAMA_PORT`.
* The LLM server ip and port should just use it's defaults by nature. If not, utilize `LLM_ENDPOINT = 127.0.0.1` and `LLM_PORT = 11434`. * The ollama ip and port should just use it's defaults by nature. If not, utilize `OLLAMA_IP = 127.0.0.1` and `OLLAMA_PORT = 11434`.
* The older `OLLAMA_IP` / `OLLAMA_PORT` names are still accepted as deprecated aliases.
* Now, you can run the bot by running `npm run client` which will build and run the decompiled typescript and run the setup for ollama. * Now, you can run the bot by running `npm run client` which will build and run the decompiled typescript and run the setup for ollama.
* **IMPORTANT**: This must be ran in the wsl/Linux instance to work properly! Using Command Prompt/Powershell/Git Bash/etc. will not work on Windows (at least in my experience). * **IMPORTANT**: This must be ran in the wsl/Linux instance to work properly! Using Command Prompt/Powershell/Git Bash/etc. will not work on Windows (at least in my experience).
* Refer to the [resources](../README.md#resources) on what node version to use. * Refer to the [resources](../README.md#resources) on what node version to use.
+1824 -1253
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -1,11 +1,12 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.9.2", "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": {
"tests": "vitest run", "tests": "vitest run",
"coverage": "vitest run --coverage",
"watch": "tsx watch src", "watch": "tsx watch src",
"build": "tsc", "build": "tsc",
"prod": "node .", "prod": "node .",
@@ -31,17 +32,15 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@vitest/coverage-v8": "^3.0.9",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vitest": "^5.0.1" "vitest": "^3.0.4"
}, },
"type": "module", "type": "module",
"engines": { "engines": {
"npm": ">=10.9.0", "npm": ">=10.9.0",
"node": ">=24.0.0" "node": ">=22.12.0"
},
"allowScripts": {
"esbuild@0.25.12": true
} }
} }
+1 -4
View File
@@ -23,11 +23,8 @@ export const ollama = new Ollama({
// Create Queue managed by Events // Create Queue managed by Events
const messageHistory: Queue<UserMessage> = new Queue<UserMessage> const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
// Create Channel History Queue managed by Events
const channelMessageHistory: Queue<UserMessage> = new Queue<UserMessage>
// register all events // register all events
registerEvents(client, Events, messageHistory, channelMessageHistory, 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)
+1 -3
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[]
-50
View File
@@ -1,50 +0,0 @@
import { Client, CommandInteraction, MessageFlags } from "discord.js";
import { accessChannelContext, 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 })
}
}
+11 -57
View File
@@ -1,6 +1,6 @@
import { TextChannel } from 'discord.js' import { TextChannel } from 'discord.js'
import { import {
event, Events, normalMessage, UserMessage, clean, addToChannelContext, 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
+2 -2
View File
@@ -2,8 +2,8 @@ import { getEnvVar } from './utils/index.js'
export const Keys = { export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'), clientToken: getEnvVar('CLIENT_TOKEN'),
ipAddress: getEnvVar('LLM_ENDPOINT', process.env.OLLAMA_IP ?? '127.0.0.1'), // OLLAMA_IP is a deprecated alias ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none
portAddress: getEnvVar('LLM_PORT', process.env.OLLAMA_PORT ?? '11434'), // OLLAMA_PORT is a deprecated alias 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
+1 -1
View File
@@ -26,7 +26,7 @@ export function getEnvVar(name: string, fallback?: string): string {
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") || name.endsWith("_ENDPOINT")) && !ipValidate.test(value)) if ((name.endsWith("_IP") || name.endsWith("_ADDRESS")) && !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
+1 -3
View File
@@ -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)
} }
-48
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
* *
-40
View File
@@ -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))
@@ -88,39 +84,3 @@ export async function normalMessage(
// 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
+1 -1
View File
@@ -27,7 +27,7 @@ describe('Commands Existence', () => {
// test specific commands in the object // test specific commands in the object
it('references specific commands', () => { it('references specific commands', () => {
const commandsString = commands.map(e => e.name).join(', ') const commandsString = commands.map(e => e.name).join(', ')
const expectedCommands = ['thread', 'private-thread', 'message-stream', 'toggle-chat', 'shutoff', 'modify-capacity', 'clear-user-channel-history', 'pull-model', 'switch-model', 'delete-model', 'summary'] 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(', ')) expect(commandsString).toBe(expectedCommands.join(', '))
}) })
}) })
+1 -1
View File
@@ -26,6 +26,6 @@ describe('Events Existence', () => {
// test specific events in the object // test specific events in the object
it('references specific events', () => { it('references specific events', () => {
const eventsString = events.map(e => e.key.toString()).join(', ') const eventsString = events.map(e => e.key.toString()).join(', ')
expect(eventsString).toBe('clientReady, messageCreate, interactionCreate, threadDelete') expect(eventsString).toBe('ready, messageCreate, interactionCreate, threadDelete')
}) })
}) })
-24
View File
@@ -47,28 +47,4 @@ describe('Environment Setup', () => {
it('throws an error if key is not found', () => { it('throws an error if key is not found', () => {
expect(() => getEnvVar('NON_EXISTENT_KEY')).toThrowError() expect(() => getEnvVar('NON_EXISTENT_KEY')).toThrowError()
}) })
// test that *_ENDPOINT keys are validated as IPv4 addresses like *_IP keys
it('validates *_ENDPOINT keys as IPv4', () => {
expect(getEnvVar('UNSET_TEST_ENDPOINT', '127.0.0.1')).toBe('127.0.0.1')
expect(() => getEnvVar('UNSET_TEST_ENDPOINT', 'not-an-ip')).toThrowError()
})
// test the deprecated alias lookup used in src/keys.ts
it('falls back to the deprecated OLLAMA_IP / OLLAMA_PORT aliases', () => {
const saved = { LLM_ENDPOINT: process.env.LLM_ENDPOINT, LLM_PORT: process.env.LLM_PORT }
delete process.env.LLM_ENDPOINT
delete process.env.LLM_PORT
process.env.OLLAMA_IP = '10.0.0.5'
process.env.OLLAMA_PORT = '17434'
try {
expect(getEnvVar('LLM_ENDPOINT', process.env.OLLAMA_IP ?? '127.0.0.1')).toBe('10.0.0.5')
expect(getEnvVar('LLM_PORT', process.env.OLLAMA_PORT ?? '11434')).toBe('17434')
} finally {
delete process.env.OLLAMA_IP
delete process.env.OLLAMA_PORT
if (saved.LLM_ENDPOINT !== undefined) process.env.LLM_ENDPOINT = saved.LLM_ENDPOINT
if (saved.LLM_PORT !== undefined) process.env.LLM_PORT = saved.LLM_PORT
}
})
}) })
+2 -1
View File
@@ -18,8 +18,9 @@
// Decompile .ts to .js into a folder named build // Decompile .ts to .js into a folder named build
"outDir": "build", "outDir": "build",
"rootDir": "src", "rootDir": "src",
"baseUrl": ".",
"paths": { "paths": {
"*": ["./node_modules/"] "*": ["node_modules/"]
} }
}, },
// environment for env vars // environment for env vars
+6 -2
View File
@@ -1,9 +1,13 @@
import { defineConfig } from 'vitest/config' import { defineConfig, configDefaults } from 'vitest/config'
// config for vitest // config for vitest
export default defineConfig({ export default defineConfig({
test: { test: {
globals: true, // <-- reduces test file imports globals: true, // <-- reduces test file imports
reporters: ['verbose'] // <-- verbose output reporters: ['verbose'], // <-- verbose output
coverage: {
exclude: [...configDefaults.exclude, 'build/*', 'tests/*'], // <-- exclude JS build
reporter: ['text-summary'] // <-- report in text-summary
}
} }
}) })