Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d570a50d46 | ||
|
|
1c8449d578 | ||
|
|
33152b33f3 | ||
|
|
1ccd1a012e | ||
|
|
68a5e097fe | ||
|
|
624ff2e5c8 | ||
|
|
9f61f6bc6c | ||
|
|
5d02800c3f | ||
|
|
5061dab335 | ||
|
|
947ff89958 | ||
|
|
6a9ee2d6d0 | ||
|
|
e3b0c9abe4 | ||
|
|
36a0cd309b | ||
|
|
b49b464afb | ||
|
|
2caf54346a | ||
|
|
6e6467c2a5 | ||
|
|
b463b0a8cb | ||
|
|
42ef38db14 | ||
|
|
af23db20bb | ||
|
|
117b195095 | ||
|
|
b361636a93 | ||
|
|
4dbd45bccd | ||
|
|
02ffb6a196 | ||
|
|
060494e883 | ||
|
|
352d88ee9d | ||
|
|
e60c2f88b8 | ||
|
|
b498276978 | ||
|
|
ae9cac65a9 | ||
|
|
61d3dc4312 |
@@ -1,15 +1,6 @@
|
||||
# Discord token for the bot
|
||||
CLIENT_TOKEN = BOT_TOKEN
|
||||
|
||||
# id token of a discord server
|
||||
GUILD_ID = GUILD_ID
|
||||
|
||||
# model for the bot to query from (i.e. llama2 [llama2:13b], mistral, codellama, etc... )
|
||||
MODEL = MODEL_NAME
|
||||
|
||||
# discord bot user id for mentions
|
||||
CLIENT_UID = BOT_USER_ID
|
||||
|
||||
# ip/port address of docker container, I use 172.18.0.3 for docker, 127.0.0.1 for local
|
||||
OLLAMA_IP = IP_ADDRESS
|
||||
OLLAMA_PORT = PORT
|
||||
|
||||
26
.github/CONTRIBUTING.md
vendored
26
.github/CONTRIBUTING.md
vendored
@@ -1,31 +1,39 @@
|
||||
<!--
|
||||
Author: Kevin Dang
|
||||
Date: 1-30-2024
|
||||
Author: Kevin Dang
|
||||
Date: 1-30-2024
|
||||
Changes:
|
||||
10-01-2024 - Jonathan Smoley
|
||||
-->
|
||||
|
||||
## Naming Conventions
|
||||
* Branches
|
||||
* prefix your branch name with the type of contribution:
|
||||
* features: `'feature/**'`
|
||||
* releases: `'releases/**'`
|
||||
* bugs: `'bug/**'`
|
||||
* docs: `'docs/**'`
|
||||
|
||||
## Run the Bot
|
||||
* Refer to all sections below before running the bot.
|
||||
* You should now have `Ollama`, `NodeJS`, ran `npm install`.
|
||||
* You will also need a discord bot to run. Refer to the [developer portal](https://discord.com/developers/) to learn how to set one up and invite it to your server. If that does not help then look up a YouTube video like this [one](https://www.youtube.com/watch?v=KZ3tIGHU314&ab_channel=UnderCtrl).
|
||||
* Now run `npm run start` to run the client and ollama at the same time (this must be one in wsl or a Linux distro)
|
||||
|
||||
* Now run `npm run client` to run the client (this must be done in wsl or a Linux distro)
|
||||
|
||||
## Set up (Development-side)
|
||||
* Pull the repository using `https://github.com/kevinthedang/discord-ollama.git`.
|
||||
* Refer to `Ollama Setup` in the readme to set up Ollama.
|
||||
* Refer to `Environment Setup` in the readme to set up Ollama.
|
||||
* This must be set up in a Linux environment or wsl2.
|
||||
* Install NodeJS `v18.18.2`
|
||||
* You can check out `Resources` and `To Run` in the readme for a bit of help.
|
||||
* You can check out `Resources` in the readme for a bit of help.
|
||||
* You can also reference [NodeJS Setup](#nodejs-setup)
|
||||
* When you have the project pulled from github, open up a terminal and run `npm i` or `npm install` to get all of the packages for the project.
|
||||
* In some kind of terminal (`git bash` is good) to run the client. You can run Ollama but opening up wsl2 and typing `ollama serve`.
|
||||
* Refer to `Ollama Setup` if there are any issues.
|
||||
|
||||
## Environment
|
||||
* You will need two environment files:
|
||||
* You will need an environment file:
|
||||
* `.env`: for running the bot
|
||||
* Please refer to `.env.sample` for all environment variables to include
|
||||
* `.env.dev.local`: also runs the bot, but with development variables
|
||||
* Currently there are no differences between the two, but when needed, you may add environment variables as needed.
|
||||
|
||||
## NodeJS Setup
|
||||
* Install [nvm](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating) using `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash`
|
||||
|
||||
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -14,4 +14,6 @@
|
||||
|
||||
## After the Pull Request is Opened
|
||||
* One the Pull Request has been created, please add any Issue(s) that are being addressed by this change (if any).
|
||||
* If the reviewer(s) mention any changes or open threads for questions, please resolve those as soon as you can.
|
||||
* If the reviewer(s) mention any changes or open threads for questions, please resolve those as soon as you can.
|
||||
|
||||
# Ensure you version increment as necessary!!!
|
||||
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -1,20 +1,9 @@
|
||||
name: Builds
|
||||
run-name: Validate Node and Docker Builds
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '/'
|
||||
- 'src/**'
|
||||
- 'tests/**'
|
||||
- '!docs/**'
|
||||
- '!imgs/**'
|
||||
- '!.github/**'
|
||||
- '.github/workflows/**'
|
||||
- '!.gitignore'
|
||||
- '!LICENSE'
|
||||
- '!README'
|
||||
- master # runs after Pull Request is merged
|
||||
|
||||
jobs:
|
||||
Discord-Node-Build: # test if the node install and run
|
||||
@@ -42,9 +31,6 @@ jobs:
|
||||
run: |
|
||||
touch .env
|
||||
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
|
||||
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
|
||||
echo MODEL = ${{ secrets.MODEL }} >> .env
|
||||
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
|
||||
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
|
||||
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
|
||||
|
||||
@@ -71,9 +57,6 @@ jobs:
|
||||
run: |
|
||||
touch .env
|
||||
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
|
||||
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
|
||||
echo MODEL = ${{ secrets.MODEL }} >> .env
|
||||
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
|
||||
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
|
||||
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
|
||||
|
||||
@@ -83,7 +66,7 @@ jobs:
|
||||
|
||||
- name: Check Images Exist
|
||||
run: |
|
||||
(docker images | grep -q 'discord/bot' && docker images | grep -qE 'ollama/ollama') || exit 1
|
||||
(docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama') || exit 1
|
||||
|
||||
- name: Check Containers Exist
|
||||
run: |
|
||||
|
||||
49
.github/workflows/coverage.yml
vendored
Normal file
49
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
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/hydrogen
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
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
|
||||
|
||||
- name: Collect Code Coverage
|
||||
run: |
|
||||
LINE_PCT=$(npm run test: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
|
||||
48
.github/workflows/release.yml
vendored
Normal file
48
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Deploy
|
||||
run-name: Release Docker Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
Release-Docker-Image:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node Environment lts/hydrogen
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
cache: "npm"
|
||||
|
||||
- name: Create Environment Variables
|
||||
run: |
|
||||
touch .env
|
||||
echo CLIENT_TOKEN = NOT_REAL_TOKEN >> .env
|
||||
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
|
||||
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
|
||||
|
||||
- name: Get Version from package.json
|
||||
run: echo "VERSION=$(jq -r '.version' package.json)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build Image
|
||||
run: |
|
||||
npm run docker:build
|
||||
|
||||
- name: Build Image as Latest
|
||||
run: |
|
||||
npm run docker:build-latest
|
||||
|
||||
- name: Log into Docker
|
||||
run: |
|
||||
docker login --username ${{ vars.DOCKER_USER }} --password ${{ secrets.DOCKER_PASS }}
|
||||
|
||||
- name: Release Docker Image
|
||||
run: |
|
||||
docker push ${{ vars.DOCKER_USER }}/discord-ollama:${{ env.VERSION }}
|
||||
docker push ${{ vars.DOCKER_USER }}/discord-ollama:latest
|
||||
37
.github/workflows/test.yml
vendored
37
.github/workflows/test.yml
vendored
@@ -5,7 +5,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '/'
|
||||
- '*'
|
||||
- 'package*.json'
|
||||
- 'src/**'
|
||||
- 'tests/**'
|
||||
- '!docs/**'
|
||||
@@ -38,43 +39,9 @@ jobs:
|
||||
run: |
|
||||
touch .env
|
||||
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
|
||||
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
|
||||
echo MODEL = ${{ secrets.MODEL }} >> .env
|
||||
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
|
||||
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
|
||||
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
|
||||
|
||||
- name: Test Application
|
||||
run: |
|
||||
npm run test:run
|
||||
|
||||
Discord-Ollama-Container-Test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node Environment lts/hydrogen
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/hydrogen
|
||||
cache: "npm"
|
||||
|
||||
- name: Create Environment Variables
|
||||
run: |
|
||||
touch .env
|
||||
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
|
||||
echo GUILD_ID = ${{ secrets.GUILD_ID }} >> .env
|
||||
echo MODEL = ${{ secrets.MODEL }} >> .env
|
||||
echo CLIENT_UID = ${{ secrets.CLIENT_UID }} >> .env
|
||||
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
|
||||
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
|
||||
|
||||
- name: Setup Docker Network and Images
|
||||
run: |
|
||||
npm run docker:start-cpu
|
||||
|
||||
- name: Test Docker Container
|
||||
run: |
|
||||
npm run docker:test
|
||||
24
CODEOWNERS
Normal file
24
CODEOWNERS
Normal file
@@ -0,0 +1,24 @@
|
||||
# The further along the ownership is, the more precedence it has.
|
||||
# This to make sure the right people look at certain changes.
|
||||
# Last Edited: 11/23/2024
|
||||
# Author: Kevin Dang
|
||||
|
||||
# These owners will be the default owners
|
||||
# for everything in the repo. However it's
|
||||
# only for the rest of the files not declared by the
|
||||
# following ownerships below.
|
||||
* @kevinthedang @JT2M0L3Y
|
||||
|
||||
# Technical/Business Code Ownership
|
||||
/src/ @kevinthedang @JT2M0L3Y
|
||||
/tests/ @kevinthedang @JT2M0L3Y
|
||||
/.github/ @kevinthedang
|
||||
|
||||
# Docker Ownership
|
||||
Dockerfile @kevinthedang
|
||||
docker-compose.yml @kevinthedang
|
||||
|
||||
# Documentation Ownership
|
||||
/docs/ @kevinthedang
|
||||
/imgs/ @kevinthedang
|
||||
|
||||
28
README.md
28
README.md
@@ -5,14 +5,16 @@
|
||||
<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/actions/workflows/build.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a>
|
||||
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/release.yml"><img alt="Release Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/release.yml/badge.svg" /></a>
|
||||
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/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>
|
||||
|
||||
## About/Goals
|
||||
Ollama is an AI model management tool that allows users to install and use custom large language models locally.
|
||||
The project aims to:
|
||||
* [x] Create a Discord bot that will utilize Ollama and chat to chat with users!
|
||||
* [ ] User Preferences on Chat
|
||||
* [x] User Preferences on Chat
|
||||
* [x] Message Persistance on Channels and Threads
|
||||
* [x] Threads
|
||||
* [x] Channels
|
||||
@@ -20,39 +22,37 @@ The project aims to:
|
||||
* [x] Slash Commands Compatible
|
||||
* [x] Generated Token Length Handling for >2000
|
||||
* [x] Token Length Handling of any message size
|
||||
* [ ] User vs. Server Preferences
|
||||
* [x] User vs. Server Preferences
|
||||
* [ ] Redis Caching
|
||||
* [x] Administrator Role Compatible
|
||||
* [ ] Multi-User Chat Generation (Multiple users chatting at the same time)
|
||||
* [ ] Automatic and Manual model pulling through the Discord client
|
||||
* [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
|
||||
* [ ] Allow others to create their own models personalized for their own servers!
|
||||
* [ ] Documentation on creating your own LLM
|
||||
* [ ] Documentation on web scrapping and cleaning
|
||||
|
||||
## Documentation
|
||||
These are guides to the feature set included and the events triggered in this app.
|
||||
* [User Slash Commands](./docs/commands-guide.md)
|
||||
* [Client Events](./docs/events-guide.md)
|
||||
|
||||
## Environment Setup
|
||||
* 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.
|
||||
* For example, `CLIENT_TOKEN = [Bot Token]`
|
||||
* Please refer to the docs for bot setup.
|
||||
* [Creating a Discord App](./docs/setup-discord-app.md)
|
||||
* [Local Machine Setup](./docs/setup-local.md)
|
||||
* [Docker Setup for Servers and Local Machines](./docs/setup-docker.md)
|
||||
* Nvidia is recommended for now, but support for other GPUs should be development.
|
||||
* Local use is not recommended.
|
||||
* [Creating a Discord App](./docs/setup-discord-app.md)
|
||||
|
||||
## Resources
|
||||
* [NodeJS](https://nodejs.org/en)
|
||||
* This project runs on `lts\hydrogen`.
|
||||
* To run dev in `ts-node`/`nodemon`, using `v18.18.2` is recommended.
|
||||
* To run dev with `tsx`, you can use `v20.10.0` or earlier.
|
||||
* This project runs on `lts\hydrogen`.
|
||||
* This project supports any NodeJS version above `16.x.x` to only allow ESModules.
|
||||
* [Ollama](https://ollama.com/)
|
||||
* [Ollama Docker Image](https://hub.docker.com/r/ollama/ollama)
|
||||
|
||||
> [!CAUTION]
|
||||
> `v18.X.X` or `lts/hydrogen` will not run properly for `npm run dev-mon`. It is recommended to just use `npm run dev-tsx` for development. The nodemon version will likely be removed in a future update.
|
||||
|
||||
* [Discord Developer Portal](https://discord.com/developers/docs/intro)
|
||||
* [Discord.js Docs](https://discord.js.org/docs/packages/discord.js/main)
|
||||
* [Setting up Docker (Ubuntu 20.04)](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
|
||||
* [Setting up Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
|
||||
@@ -61,4 +61,4 @@ The project aims to:
|
||||
* [Kevin Dang](https://github.com/kevinthedang)
|
||||
* [Jonathan Smoley](https://github.com/JT2M0L3Y)
|
||||
|
||||
[discord-ollama](https://github.com/kevinthedang/discord-ollama) © 2023 by [Kevin Dang](https://github.com/kevinthedang) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
||||
[discord-ollama](https://github.com/kevinthedang/discord-ollama) © 2023 by [Kevin Dang](https://github.com/kevinthedang) is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# creates the docker compose
|
||||
version: '3.7'
|
||||
# creates the docker compose
|
||||
|
||||
# build individual services
|
||||
services:
|
||||
@@ -8,12 +7,9 @@ services:
|
||||
build: ./ # find docker file in designated path
|
||||
container_name: discord
|
||||
restart: always # rebuild container always
|
||||
image: discord/bot:0.5.4
|
||||
image: kevinthedang/discord-ollama:0.7.3
|
||||
environment:
|
||||
CLIENT_TOKEN: ${CLIENT_TOKEN}
|
||||
GUILD_ID: ${GUILD_ID}
|
||||
MODEL: ${MODEL}
|
||||
CLIENT_UID: ${CLIENT_UID}
|
||||
OLLAMA_IP: ${OLLAMA_IP}
|
||||
OLLAMA_PORT: ${OLLAMA_PORT}
|
||||
networks:
|
||||
|
||||
105
docs/commands-guide.md
Normal file
105
docs/commands-guide.md
Normal file
@@ -0,0 +1,105 @@
|
||||
## Commands Guide
|
||||
This is a guide to all of the slash commands for the app.
|
||||
|
||||
* Action Commands are commands that do not affect a user's `preference file`.
|
||||
* Guild Commands can also be considered action commands.
|
||||
|
||||
> [!NOTE]
|
||||
> Administrator commands are only usable by actual administrators on the Discord server.
|
||||
|
||||
### Guild Commands (Administrator)
|
||||
1. Disable (or Toggle Chat)
|
||||
This command will `enable` or `disable` whether or not the app will respond to users.
|
||||
|
||||
```
|
||||
/toggle-chat enabled true
|
||||
```
|
||||
|
||||
2. Shutoff
|
||||
This command will shutoff the app so no users can converse with it.
|
||||
The app must be manually restarted upon being shutoff.
|
||||
|
||||
Below shuts off the app by putting `true` in the `are-your-sure` field.
|
||||
|
||||
```
|
||||
/shutoff are-you-sure true
|
||||
```
|
||||
|
||||
### Action Commands
|
||||
1. Clear Channel (Message) History
|
||||
This command will clear the history of the current channel for the user that calls it.
|
||||
Running the command in any channel will clear the message history.
|
||||
|
||||
```
|
||||
/clear-user-channel-history
|
||||
```
|
||||
|
||||
2. Pull Model
|
||||
This command will pull a model that exists on the [Ollama Model Library](https://ollama.com/library). If it does not exist there, it will throw a hissy fit.
|
||||
|
||||
Below trys to pull the `codellama` model.
|
||||
|
||||
```
|
||||
/pull-model model-to-pull codellama
|
||||
```
|
||||
|
||||
3. Thread Create
|
||||
This command creates a public thread to talk with the app instead of using a `GuildText` channel.
|
||||
|
||||
```
|
||||
/thread
|
||||
```
|
||||
|
||||
4. (Private) Thread Create
|
||||
This command creates a private thread to talk with the bot privately.
|
||||
Invite others to the channel and they will be able to talk to the app as well.
|
||||
|
||||
```
|
||||
/private-thread
|
||||
```
|
||||
|
||||
### User Preference Commands
|
||||
1. Capacity
|
||||
This command changes how much context it will keep in conversations with the app.
|
||||
This is applied for all of existing chats when interacting with the app.
|
||||
|
||||
Below sets the message history capacity to at most 5 messages at once.
|
||||
|
||||
```
|
||||
/modify-capacity context-capacity 5
|
||||
```
|
||||
|
||||
2. Message Stream
|
||||
This command will toggle whether or not the app will "stream" a response.
|
||||
(think of how ChatGPT and other interfaces do this)
|
||||
|
||||
Below sets the `stream` to true to make the app respond in increments.
|
||||
|
||||
```
|
||||
/message-stream stream true
|
||||
```
|
||||
> [!NOTE]
|
||||
> This is a very slow progress on Discord because "spamming" changes within 5 seconds is not allowed.
|
||||
|
||||
3. Message Style
|
||||
This command allows a user to select whether to embed the app's response.
|
||||
|
||||
```
|
||||
/message-style embed true
|
||||
```
|
||||
|
||||
This allows the app to respond as a user would normally respond.
|
||||
|
||||
```
|
||||
/message-style embed false
|
||||
```
|
||||
|
||||
4. Switch Model
|
||||
This command will switch the user-preferred model so long as it exists in within the local ollama service or from the [Ollama Model Library](https://ollama.com/library).
|
||||
If it cannot be found locally, it will attempt to find it in the model library.
|
||||
|
||||
Below we are trying to switch to a specific model size.
|
||||
|
||||
```
|
||||
/switch-model model-to-use llama3.2:1.3b
|
||||
```
|
||||
27
docs/events-guide.md
Normal file
27
docs/events-guide.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Events Guide
|
||||
This is a guide to all of the client events for the app.
|
||||
|
||||
> [!NOTE] Each of these is logged to the console for a developer to track.
|
||||
|
||||
1. ClientReady
|
||||
This event signifies that the Discord app is online.
|
||||
Here the app's activity is set and its commands are registered.
|
||||
|
||||
2. InteractionCreate
|
||||
This event signifies that a user interacted from Discord in some way.
|
||||
Here commands are selected from a knowledge bank and executed if found.
|
||||
|
||||
> [!NOTE] Possible interactions include commands, buttons, menus, etc.
|
||||
|
||||
3. MessageCreate
|
||||
This event signifies that a message was sent.
|
||||
Here user questions and comments for the LLM are processed.
|
||||
1. check message is from a user and mentions the app
|
||||
2. check for interaction preferences
|
||||
3. add the message to a queue
|
||||
4. check the response for success
|
||||
5. send a response back to the user.
|
||||
|
||||
4. ThreadDelete
|
||||
This event signifies that a Discord Thread was deleted.
|
||||
Here any preferences set for interaction within the thread are cleared away.
|
||||
@@ -18,10 +18,6 @@
|
||||
|
||||

|
||||
|
||||
* You will also need your App's **Client ID**, navigate to **OAuth2** and copy your id.
|
||||
|
||||

|
||||
|
||||
* That should be all of the environment variables needed from Discord, now we need this app on your server.
|
||||
* Navigate to **Installation** and Copy the provided **Install Link** to allow your App to your server.
|
||||
* You should set the **Guild Install** permissions as you like, for this purpose we will allow admin priviledges for now. Ensure the **bot** scope is added to do this.
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
* Follow this guide to setup [Docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
|
||||
* If on Windows, download [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/) to get the docker engine.
|
||||
* Please also install [Docker Compose](https://docs.docker.com/compose/install/linux/) for easy running. If not, there are [scripts](#manual-run-with-docker) to set everything up.
|
||||
* **IMPORTANT NOTE**: Currently, it seems like wsl does not like Nvidia Container Toolkit. It will work initially then reset it for some odd reason. For now, it is advised to use an actually Linux machine to run using Docker. If you do not care about utilizing your GPU or don't even have a Nvidia GPU then disregard this.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Currently, it seems like wsl does not like Nvidia Container Toolkit. It will work initially then reset it for some odd reason. For now, it is advised to use an actually Linux machine to run using Docker. If you do not care about utilizing your GPU or don't even have a Nvidia GPU then disregard this.
|
||||
|
||||
## Nvidia Container Toolkit Setup
|
||||
### Installation with Apt
|
||||
@@ -48,15 +50,13 @@ sudo systemctl restart docker
|
||||
* `SUBNET_ADDRESS = 172.18.0.0`
|
||||
* 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 will need a model in the container for this to work properly, on Docker Desktop go to the `Containers` tab, select the `ollama` container, and select `Exec` to run as root on your container. Now, run `ollama pull [model name]` to get your model.
|
||||
* For Linux Servers, you need another shell to pull the model, or if you run `docker compose build && docker compose up -d`, then it will run in the background to keep your shell. Run `docker exec -it ollama bash` to get into the container and run the samme pull command above.
|
||||
* 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.
|
||||
* For cleaning up on Linux (or Windows), run the following commands:
|
||||
* `docker compose stop`
|
||||
* `docker compose rm`
|
||||
* `docker ps` to check if containers have been removed.
|
||||
* This may not work if the nvidia installation was done incorrectly. If this is the case, please utilize the [Manual "Clean-up"](#manual-run-with-docker) shown below.
|
||||
* You can also use `npm run clean` to clean up the containers and remove the network to address a possible `Address already in use` problem.
|
||||
* You can also use `npm run clean` to clean up the containers and remove the network to address a possible `Address already in use` problem. This script does not have to end without error to work.
|
||||
|
||||
## Manual Run (with Docker)
|
||||
* Run the following commands:
|
||||
|
||||
@@ -11,14 +11,17 @@
|
||||
* You can now interact with the model you just ran (it might take a second to startup).
|
||||
* Response time varies with processing power!
|
||||
|
||||
> [!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)
|
||||
|
||||
## To Run Locally (without Docker)
|
||||
* Run `npm install` to install the npm packages.
|
||||
* Ensure that your [.env](../.env.sample) file's `OLLAMA_IP` is `127.0.0.1` to work properly.
|
||||
* You only need your `CLIENT_TOKEN`, `GUILD_ID`, `MODEL`, `CLIENT_UID`, `OLLAMA_IP`, `OLLAMA_PORT`.
|
||||
* You only need your `CLIENT_TOKEN`, `OLLAMA_IP`, `OLLAMA_PORT`.
|
||||
* 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`.
|
||||
* 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).
|
||||
* Refer to the [resources](../README.md#resources) on what node version to use.
|
||||
* If you are using wsl, open up a separate terminal/shell to startup the ollama service. Again, if you are running an older ollama, you must run `ollama serve` in that shell.
|
||||
* If you are on an actual Linux machine/VM there is no need for another terminal (unless you have an older ollama version).
|
||||
* If you do not have a model, you will need to run `ollama pull [model name]` in a separate terminal to get it.
|
||||
* If you do not have a model, you **can optionally** run `ollama pull [model name]` in wsl prior to application start. You are not required as it can be pulled from the Discord client.
|
||||
12
nodemon.json
12
nodemon.json
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"restartable": "rs",
|
||||
"ignore": ["node_modules/"],
|
||||
"watch": ["src/"],
|
||||
"execMap": {
|
||||
"ts": "ts-node --esm"
|
||||
},
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"ext": "js,json,ts"
|
||||
}
|
||||
2747
package-lock.json
generated
2747
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,44 +1,42 @@
|
||||
{
|
||||
"name": "discord-ollama",
|
||||
"version": "0.5.4",
|
||||
"version": "0.7.3",
|
||||
"description": "Ollama Integration into discord",
|
||||
"main": "build/index.js",
|
||||
"exports": "./build/index.js",
|
||||
"scripts": {
|
||||
"dev-tsx": "tsx watch src/index.ts",
|
||||
"dev-mon": "nodemon --config nodemon.json src/index.ts",
|
||||
"build": "tsc",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"watch": "tsx watch src",
|
||||
"build": "tsc",
|
||||
"prod": "node .",
|
||||
"client": "npm run build && npm run prod",
|
||||
"clean": "docker compose down && docker rmi $(docker images | grep $(node -p \"require('./package.json').version\") | 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",
|
||||
"docker:start": "npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama",
|
||||
"docker:start-cpu": "npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama-cpu",
|
||||
"docker:clean": "docker rm -f discord && docker rm -f ollama && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
|
||||
"docker:clean": "docker rm -f discord && docker rm -f ollama && docker network prune -f && docker rmi $(docker images | grep kevinthedang | tr -s ' ' | cut -d ' ' -f 3) && docker rmi $(docker images --filter \"dangling=true\" -q --no-trunc)",
|
||||
"docker:network": "docker network create --subnet=172.18.0.0/16 ollama-net",
|
||||
"docker:build": "docker build --no-cache -t discord/bot:$(node -p \"require('./package.json').version\") .",
|
||||
"docker:test": "docker run -d --rm -v discord:/src/app --name test discord/bot:$(node -p \"require('./package.json').version\") npm run test:run",
|
||||
"docker:client": "docker run -d -v discord:/src/app --name discord --network ollama-net --ip 172.18.0.3 discord/bot:$(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: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: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-cpu": "docker network prune -f && npm run docker:network && npm run docker:build && npm run docker:client && npm run docker:ollama-cpu"
|
||||
},
|
||||
"author": "Kevin Dang",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.15.3",
|
||||
"discord.js": "^14.16.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"ollama": "^0.5.2"
|
||||
"ollama": "^0.5.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.2",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"nodemon": "^3.1.3",
|
||||
"@types/node": "^22.9.0",
|
||||
"@vitest/coverage-v8": "^2.1.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.15.5",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.6.0"
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3",
|
||||
"vitest": "^2.1.4"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Client, GatewayIntentBits } from 'discord.js'
|
||||
import { UserMessage, registerEvents } from './utils/events.js'
|
||||
import Events from './events/index.js'
|
||||
import { Ollama } from 'ollama'
|
||||
import { Queue } from './queues/queue.js'
|
||||
|
||||
// Import keys/tokens
|
||||
import { UserMessage, registerEvents } from './utils/index.js'
|
||||
import Events from './events/index.js'
|
||||
import Keys from './keys.js'
|
||||
|
||||
|
||||
@@ -19,21 +17,15 @@ const client = new Client({
|
||||
});
|
||||
|
||||
// initialize connection to ollama container
|
||||
const ollama = new Ollama({
|
||||
export const ollama = new Ollama({
|
||||
host: `http://${Keys.ipAddress}:${Keys.portAddress}`,
|
||||
})
|
||||
|
||||
// Create Queue managed by Events
|
||||
const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
|
||||
|
||||
/**
|
||||
* register events for bot to listen to in discord
|
||||
* @param messageHistory message history for the llm
|
||||
* @param Events events to register
|
||||
* @param client the bot reference
|
||||
* @param Keys tokens from .env files
|
||||
*/
|
||||
registerEvents(client, Events, messageHistory, Keys, ollama)
|
||||
// register all events
|
||||
registerEvents(client, Events, messageHistory, ollama)
|
||||
|
||||
// Try to log in the client
|
||||
await client.login(Keys.clientToken)
|
||||
@@ -45,5 +37,6 @@ await client.login(Keys.clientToken)
|
||||
// queue up bots name
|
||||
messageHistory.enqueue({
|
||||
role: 'assistant',
|
||||
content: `My name is ${client.user?.username}`
|
||||
content: `My name is ${client.user?.username}`,
|
||||
images: []
|
||||
})
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openConfig } from '../utils/jsonHandler.js'
|
||||
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
|
||||
|
||||
export const Capacity: SlashCommand = {
|
||||
name: 'modify-capacity',
|
||||
description: 'number of messages bot will hold for context.',
|
||||
description: 'maximum amount messages bot will hold for context.',
|
||||
|
||||
// set available user options to pass to the command
|
||||
options: [
|
||||
{
|
||||
name: 'context-capacity',
|
||||
description: 'a number to set capacity',
|
||||
description: 'number of allowed messages to remember',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true
|
||||
}
|
||||
@@ -20,13 +19,13 @@ export const Capacity: SlashCommand = {
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch channel and message
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
|
||||
if (!channel || !UserCommand.includes(channel.type)) return
|
||||
|
||||
// set state of bot chat features
|
||||
openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('context-capacity')?.value)
|
||||
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('context-capacity')?.value)
|
||||
|
||||
interaction.reply({
|
||||
content: `Message History Capacity has been set to \`${interaction.options.get('context-capacity')?.value}\``,
|
||||
content: `Max message history is now set to \`${interaction.options.get('context-capacity')?.value}\``,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openConfig } from '../utils/jsonHandler.js'
|
||||
|
||||
export const ChannelToggle: SlashCommand = {
|
||||
name: 'channel-toggle',
|
||||
description: 'toggles channel or thread usage.',
|
||||
|
||||
// set user option for toggling
|
||||
options: [
|
||||
{
|
||||
name: 'toggle-channel',
|
||||
description: 'toggle channel usage, otherwise threads',
|
||||
type: ApplicationCommandOptionType.Boolean,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
|
||||
// Query for chatting preference
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch channel location
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
|
||||
|
||||
|
||||
// set state of bot channel preferences
|
||||
openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('toggle-channel')?.value)
|
||||
|
||||
interaction.reply({
|
||||
content: `Channel Preferences have for Regular Channels set to \`${interaction.options.get('toggle-channel')?.value}\``,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
}
|
||||
33
src/commands/cleanUserChannelHistory.ts
Normal file
33
src/commands/cleanUserChannelHistory.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Channel, Client, CommandInteraction, TextChannel } from 'discord.js'
|
||||
import { clearChannelInfo, SlashCommand, UserCommand } from '../utils/index.js'
|
||||
|
||||
export const ClearUserChannelHistory: SlashCommand = {
|
||||
name: 'clear-user-channel-history',
|
||||
description: 'clears history for user in the current channel',
|
||||
|
||||
// Clear channel history for intended user
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch current channel
|
||||
const channel: Channel | null = await client.channels.fetch(interaction.channelId)
|
||||
|
||||
// if not an existing channel or a GuildText, fail command
|
||||
if (!channel || !UserCommand.includes(channel.type)) return
|
||||
|
||||
// clear channel info for user
|
||||
const successfulWipe = await clearChannelInfo(interaction.channelId,
|
||||
interaction.channel as TextChannel,
|
||||
interaction.user.username)
|
||||
|
||||
// check result of clearing history
|
||||
if (successfulWipe)
|
||||
interaction.reply({
|
||||
content: `History cleared in **this channel** cleared for **${interaction.user.username}**.`,
|
||||
ephemeral: true
|
||||
})
|
||||
else
|
||||
interaction.reply({
|
||||
content: `History was not be found for **${interaction.user.username}** in **this channel**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openConfig } from '../utils/jsonHandler.js'
|
||||
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { AdminCommand, openConfig, SlashCommand } from '../utils/index.js'
|
||||
|
||||
export const Disable: SlashCommand = {
|
||||
name: 'toggle-chat',
|
||||
description: 'toggle all chat features, Adminstrator Only.',
|
||||
description: 'toggle all chat features. Adminstrator Only.',
|
||||
|
||||
// set available user options to pass to the command
|
||||
options: [
|
||||
@@ -20,12 +19,12 @@ export const Disable: SlashCommand = {
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch channel and message
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== ChannelType.GuildText) return
|
||||
if (!channel || !AdminCommand.includes(channel.type)) return
|
||||
|
||||
// check if runner is an admin
|
||||
if (!interaction.memberPermissions?.has('Administrator')) {
|
||||
interaction.reply({
|
||||
content: `${interaction.commandName} is an Administrator Command.\n\nYou, ${interaction.member?.user.username}, are not an Administrator in this server.\nPlease contact an admin to use this command.`,
|
||||
content: `${interaction.commandName} is an admin command.\n\nPlease contact an admin to use this command for you.`,
|
||||
ephemeral: true
|
||||
})
|
||||
return
|
||||
@@ -35,7 +34,7 @@ export const Disable: SlashCommand = {
|
||||
openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('enabled')?.value)
|
||||
|
||||
interaction.reply({
|
||||
content: `Chat features has been \`${interaction.options.get('enabled')?.value ? "enabled" : "disabled" }\``,
|
||||
content: `${client.user?.username} is now **${interaction.options.get('enabled')?.value ? "enabled" : "disabled" }**.`,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { Disable } from './disable.js'
|
||||
import { Shutoff } from './shutoff.js'
|
||||
import { Capacity } from './capacity.js'
|
||||
import { PrivateThreadCreate } from './threadPrivateCreate.js'
|
||||
import { ChannelToggle } from './channelToggle.js'
|
||||
import { ClearUserChannelHistory } from './cleanUserChannelHistory.js'
|
||||
import { PullModel } from './pullModel.js'
|
||||
import { SwitchModel } from './switchModel.js'
|
||||
|
||||
export default [
|
||||
ThreadCreate,
|
||||
@@ -16,5 +18,7 @@ export default [
|
||||
Disable,
|
||||
Shutoff,
|
||||
Capacity,
|
||||
ChannelToggle
|
||||
ClearUserChannelHistory,
|
||||
PullModel,
|
||||
SwitchModel
|
||||
] as SlashCommand[]
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openConfig } from '../utils/jsonHandler.js'
|
||||
import { ApplicationCommandOptionType, Client, CommandInteraction } from 'discord.js'
|
||||
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
|
||||
|
||||
export const MessageStream: SlashCommand = {
|
||||
name: 'message-stream',
|
||||
description: 'change preference on message streaming from ollama. WARNING: can be very slow.',
|
||||
description: 'change preference on message streaming from ollama. WARNING: can be very slow due to Discord limits.',
|
||||
|
||||
// user option(s) for setting stream
|
||||
options: [
|
||||
{
|
||||
name: 'stream',
|
||||
description: 'enable or disable stream preference',
|
||||
description: 'enable or disable message streaming',
|
||||
type: ApplicationCommandOptionType.Boolean,
|
||||
required: true
|
||||
}
|
||||
@@ -20,13 +19,13 @@ export const MessageStream: SlashCommand = {
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// verify channel
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
|
||||
if (!channel || !UserCommand.includes(channel.type)) return
|
||||
|
||||
// save value to json and write to it
|
||||
openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('stream')?.value)
|
||||
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('stream')?.value)
|
||||
|
||||
interaction.reply({
|
||||
content: `Message streaming preferences set to: \`${interaction.options.get('stream')?.value}\``,
|
||||
content: `Message streaming is now set to: \`${interaction.options.get('stream')?.value}\``,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openConfig } from '../utils/jsonHandler.js'
|
||||
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
|
||||
|
||||
export const MessageStyle: SlashCommand = {
|
||||
name: 'message-style',
|
||||
@@ -20,10 +19,10 @@ export const MessageStyle: SlashCommand = {
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch channel and message
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
|
||||
if (!channel || !UserCommand.includes(channel.type)) return
|
||||
|
||||
// set the message style
|
||||
openConfig(`${interaction.client.user.username}-config.json`, interaction.commandName, interaction.options.get('embed')?.value)
|
||||
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('embed')?.value)
|
||||
|
||||
interaction.reply({
|
||||
content: `Message style preferences for embed set to: \`${interaction.options.get('embed')?.value}\``,
|
||||
|
||||
70
src/commands/pullModel.ts
Normal file
70
src/commands/pullModel.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js";
|
||||
import { SlashCommand } from "../utils/commands.js";
|
||||
import { ollama } from "../client.js";
|
||||
import { ModelResponse } from "ollama";
|
||||
import { UserCommand } from "../utils/index.js";
|
||||
|
||||
export const PullModel: SlashCommand = {
|
||||
name: 'pull-model',
|
||||
description: 'pulls a model from the ollama model library. Administrator Only',
|
||||
|
||||
// set available user options to pass to the command
|
||||
options: [
|
||||
{
|
||||
name: 'model-to-pull',
|
||||
description: 'the name of the model to pull',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
|
||||
// Pull for model from Ollama library
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// defer reply to avoid timeout
|
||||
await interaction.deferReply()
|
||||
const modelInput: string = interaction.options.get('model-to-pull')!!.value as string
|
||||
|
||||
// fetch channel and message
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || !UserCommand.includes(channel.type)) return
|
||||
|
||||
// Admin Command
|
||||
if (!interaction.memberPermissions?.has('Administrator')) {
|
||||
interaction.reply({
|
||||
content: `${interaction.commandName} is an admin command.\n\nPlease contact a server admin to pull the model you want.`,
|
||||
ephemeral: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// check if model was already pulled
|
||||
const modelExists: boolean = await ollama.list()
|
||||
.then(response => response.models.some((model: ModelResponse) => model.name.startsWith(modelInput)))
|
||||
|
||||
try {
|
||||
// call ollama to pull desired model
|
||||
if (!modelExists) {
|
||||
interaction.editReply({
|
||||
content: `**${modelInput}** could not be found. Please wait patiently as I try to retrieve it...`
|
||||
})
|
||||
await ollama.pull({ model: modelInput })
|
||||
}
|
||||
} catch (error) {
|
||||
// could not resolve pull or model unfound
|
||||
interaction.editReply({
|
||||
content: `Could not retrieve the **${modelInput}** model. You can find models at [Ollama Model Library](https://ollama.com/library).\n\nPlease check the model library and try again.`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// successful interaction
|
||||
if (modelExists)
|
||||
interaction.editReply({
|
||||
content: `**${modelInput}** is already available.`
|
||||
})
|
||||
else
|
||||
interaction.editReply({
|
||||
content: `Successfully added **${modelInput}**.`
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { AdminCommand } from '../utils/index.js'
|
||||
|
||||
export const Shutoff: SlashCommand = {
|
||||
name: 'shutoff',
|
||||
description: 'shutdown the bot. You will need to manually bring it online again. Administrator Only.',
|
||||
|
||||
// set available user options to pass to the command
|
||||
options: [
|
||||
{
|
||||
name: 'are-you-sure',
|
||||
description: 'true = yes, false = I\'m scared',
|
||||
type: ApplicationCommandOptionType.Boolean,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
|
||||
// Query for message information and set the style
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch channel and message
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== ChannelType.GuildText) return
|
||||
if (!channel || !AdminCommand.includes(channel.type)) return
|
||||
|
||||
// log this, this will probably be improtant for logging who did this
|
||||
console.log(`User -> ${interaction.user.tag} attempting to shutdown ${client.user!!.tag}`)
|
||||
console.log(`[Command: shutoff] User ${interaction.user.tag} attempting to shutdown ${client.user!!.tag}`)
|
||||
|
||||
// check if admin or false on shutdown
|
||||
if (!interaction.memberPermissions?.has('Administrator')) {
|
||||
@@ -31,19 +22,14 @@ export const Shutoff: SlashCommand = {
|
||||
ephemeral: true
|
||||
})
|
||||
return // stop from shutting down
|
||||
} else if (!interaction.options.get('are-you-sure')?.value) {
|
||||
interaction.reply({
|
||||
content: `**Shutdown Aborted:**\n\n${interaction.user.tag}, You didn't want to shutoff **${client.user?.tag}**.`,
|
||||
ephemeral: true
|
||||
})
|
||||
return // chickened out
|
||||
}
|
||||
|
||||
// Shutoff cleared, do it
|
||||
interaction.reply({
|
||||
content: `${client.user?.tag} is ${interaction.options.get('are-you-sure')?.value ? "shutting down now." : "not shutting down." }`,
|
||||
content: `${client.user?.tag} is shutting down.`,
|
||||
ephemeral: true
|
||||
})
|
||||
console.log(`[Command: shutoff] ${client.user?.tag} is shutting down.`)
|
||||
|
||||
// clean up client instance and stop
|
||||
client.destroy()
|
||||
|
||||
68
src/commands/switchModel.ts
Normal file
68
src/commands/switchModel.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js";
|
||||
import { SlashCommand } from "../utils/commands.js";
|
||||
import { ollama } from "../client.js";
|
||||
import { ModelResponse } from "ollama";
|
||||
import { openConfig, UserCommand } from "../utils/index.js";
|
||||
|
||||
export const SwitchModel: SlashCommand = {
|
||||
name: 'switch-model',
|
||||
description: 'switches current model to use.',
|
||||
|
||||
// set available user options to pass to the command
|
||||
options: [
|
||||
{
|
||||
name: 'model-to-use',
|
||||
description: 'the name of the model to use',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
|
||||
// Switch user preferred model if available in local library
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
await interaction.deferReply()
|
||||
|
||||
const modelInput: string = interaction.options.get('model-to-use')!!.value as string
|
||||
|
||||
// fetch channel and message
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || !UserCommand.includes(channel.type)) return
|
||||
|
||||
try {
|
||||
// Phase 1: Switch to the model
|
||||
let switchSuccess = false
|
||||
await ollama.list()
|
||||
.then(response => {
|
||||
for (const model in response.models) {
|
||||
const currentModel: ModelResponse = response.models[model]
|
||||
if (currentModel.name.startsWith(modelInput)) {
|
||||
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, modelInput)
|
||||
|
||||
// successful switch
|
||||
interaction.editReply({
|
||||
content: `Successfully switched to **${modelInput}** as the preferred model for ${interaction.user.username}.`
|
||||
})
|
||||
switchSuccess = true
|
||||
}
|
||||
}
|
||||
})
|
||||
// todo: problem can be here if async messes up
|
||||
if (switchSuccess) {
|
||||
// set model now that it exists
|
||||
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, modelInput)
|
||||
return
|
||||
}
|
||||
|
||||
// Phase 2: Notify user of failure to find model.
|
||||
interaction.editReply({
|
||||
content: `Could not find **${modelInput}** in local model library.\n\nPlease contact an server admin for access to this model.`
|
||||
})
|
||||
} catch (error) {
|
||||
// could not resolve user model switch
|
||||
interaction.editReply({
|
||||
content: `Unable to switch user preferred model to **${modelInput}**.\n\n${error}\n\nPossible solution is to request an server admin run \`/pull-model ${modelInput}\` and try again.`
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openThreadInfo } from '../utils/jsonHandler.js'
|
||||
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
|
||||
import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js'
|
||||
|
||||
export const ThreadCreate: SlashCommand = {
|
||||
name: 'thread',
|
||||
@@ -10,7 +9,7 @@ export const ThreadCreate: SlashCommand = {
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch the channel
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== ChannelType.GuildText) return
|
||||
if (!channel || !AdminCommand.includes(channel.type)) return
|
||||
|
||||
const thread = await (channel as TextChannel).threads.create({
|
||||
name: `${client.user?.username}-support-${Date.now()}`,
|
||||
@@ -19,15 +18,16 @@ export const ThreadCreate: SlashCommand = {
|
||||
})
|
||||
|
||||
// Send a message in the thread
|
||||
thread.send(`Hello ${interaction.user} and others! \n\nIt's nice to meet you. Please talk to me by typing **@${client.user?.username}** with your prompt.`)
|
||||
thread.send(`Hello ${interaction.user} and others! \n\nIt's nice to meet you. Please talk to me by typing **@${client.user?.username}** with your message.`)
|
||||
|
||||
// handle storing this chat channel
|
||||
// store: thread.id, thread.name
|
||||
openThreadInfo(`${thread.id}.json`, thread)
|
||||
openChannelInfo(thread.id,
|
||||
thread as ThreadChannel,
|
||||
interaction.user.tag)
|
||||
|
||||
// user only reply
|
||||
return interaction.reply({
|
||||
content: `I can help you in thread **${thread.id}** below.`,
|
||||
content: `I can help you in <#${thread.id}> below.`,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
|
||||
import { SlashCommand } from '../utils/commands.js'
|
||||
import { openThreadInfo } from '../utils/jsonHandler.js'
|
||||
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
|
||||
import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js'
|
||||
|
||||
export const PrivateThreadCreate: SlashCommand = {
|
||||
name: 'private-thread',
|
||||
@@ -10,7 +9,7 @@ export const PrivateThreadCreate: SlashCommand = {
|
||||
run: async (client: Client, interaction: CommandInteraction) => {
|
||||
// fetch the channel
|
||||
const channel = await client.channels.fetch(interaction.channelId)
|
||||
if (!channel || channel.type !== ChannelType.GuildText) return
|
||||
if (!channel || !AdminCommand.includes(channel.type)) return
|
||||
|
||||
const thread = await (channel as TextChannel).threads.create({
|
||||
name: `${client.user?.username}-private-support-${Date.now()}`,
|
||||
@@ -23,11 +22,14 @@ export const PrivateThreadCreate: SlashCommand = {
|
||||
|
||||
// handle storing this chat channel
|
||||
// store: thread.id, thread.name
|
||||
openThreadInfo(`${thread.id}.json`, thread)
|
||||
openChannelInfo(thread.id,
|
||||
thread as ThreadChannel,
|
||||
interaction.user.tag
|
||||
)
|
||||
|
||||
// user only reply
|
||||
return interaction.reply({
|
||||
content: `I can help you in thread **${thread.id}**. Please refer to the private channel below this one.`,
|
||||
content: `I can help you in <#${thread.id}>.`,
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.js'
|
||||
import { getChannelInfo, getServerConfig, getThread, getUserConfig, openChannelInfo, openConfig, openThreadInfo, ServerConfig, UserConfig } from '../utils/jsonHandler.js'
|
||||
import { clean } from '../utils/mentionClean.js'
|
||||
import { TextChannel, ThreadChannel } from 'discord.js'
|
||||
import { TextChannel } from 'discord.js'
|
||||
import { embedMessage, event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
|
||||
import { getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, openConfig, UserConfig, getAttachmentData } from '../utils/index.js'
|
||||
|
||||
/**
|
||||
* Max Message length for free users is 2000 characters (bot or not).
|
||||
@@ -9,29 +8,28 @@ import { TextChannel, ThreadChannel } from 'discord.js'
|
||||
*
|
||||
* @param message the message received from the channel
|
||||
*/
|
||||
export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama, client }, message) => {
|
||||
log(`Message \"${clean(message.content)}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
|
||||
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client }, message) => {
|
||||
const clientId = client.user!!.id
|
||||
const cleanedMessage = clean(message.content, clientId)
|
||||
log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
|
||||
|
||||
// Do not respond if bot talks in the chat
|
||||
if (message.author.username === message.client.user.username) return
|
||||
|
||||
// Only respond if message mentions the bot
|
||||
if (!message.mentions.has(tokens.clientUid)) return
|
||||
if (!message.mentions.has(clientId)) return
|
||||
|
||||
// default stream to false
|
||||
let shouldStream = false
|
||||
|
||||
try {
|
||||
// Retrieve Server/Guild Preferences
|
||||
const serverConfig: ServerConfig = await new Promise((resolve, reject) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
getServerConfig(`${message.guildId}-config.json`, (config) => {
|
||||
// check if config.json exists
|
||||
if (config === undefined) {
|
||||
// Allowing chat options to be available
|
||||
openConfig(`${message.guildId}-config.json`, 'toggle-chat', true)
|
||||
|
||||
// default to channel scope chats
|
||||
openConfig(`${message.guildId}-config.json`, 'channel-toggle', true)
|
||||
reject(new Error('No Server Preferences is set up.\n\nCreating default server preferences file...\nPlease try chatting again.'))
|
||||
return
|
||||
}
|
||||
@@ -42,14 +40,6 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
|
||||
return
|
||||
}
|
||||
|
||||
// ensure channel json exists, if not create it
|
||||
if (config.options['channel-toggle']) {
|
||||
openChannelInfo(message.channelId,
|
||||
message.channel as TextChannel,
|
||||
message.author.username
|
||||
)
|
||||
}
|
||||
|
||||
resolve(config)
|
||||
})
|
||||
})
|
||||
@@ -59,7 +49,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
|
||||
getUserConfig(`${message.author.username}-config.json`, (config) => {
|
||||
if (config === undefined) {
|
||||
openConfig(`${message.author.username}-config.json`, 'message-style', false)
|
||||
reject(new Error('No User Preferences is set up.\n\nCreating preferences file with \`message-style\` set as \`false\` for regular messages.\nPlease try chatting again.'))
|
||||
reject(new Error('No User Preferences is set up.\n\nCreating preferences file with \`message-style\` set as \`false\` for regular message style.\nPlease try chatting again.'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,36 +65,51 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
|
||||
|
||||
// set stream state
|
||||
shouldStream = config.options['message-stream'] as boolean || false
|
||||
|
||||
if (typeof config.options['switch-model'] !== 'string')
|
||||
reject(new Error(`No Model was set. Please set a model by running \`/switch-model <model of choice>\`.\n\nIf you do not have any models. Run \`/pull-model <model name>\`.`))
|
||||
|
||||
resolve(config)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
// need new check for "open/active" threads/channels here!
|
||||
const chatMessages: UserMessage[] = await new Promise((resolve) => {
|
||||
let chatMessages: UserMessage[] = await new Promise((resolve) => {
|
||||
// set new queue to modify
|
||||
if (serverConfig.options['channel-toggle']) {
|
||||
getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
|
||||
if (channelInfo?.messages)
|
||||
resolve(channelInfo.messages)
|
||||
else {
|
||||
log(`Channel/Thread ${message.channel}-${message.author.username} does not exist. File will be created shortly...`)
|
||||
resolve([])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (chatMessages.length === 0) {
|
||||
chatMessages = await new Promise((resolve, reject) => {
|
||||
openChannelInfo(message.channelId,
|
||||
message.channel as TextChannel,
|
||||
message.author.tag
|
||||
)
|
||||
getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
|
||||
if (channelInfo?.messages)
|
||||
resolve(channelInfo.messages)
|
||||
else
|
||||
log(`Channel ${message.channel}-${message.author.username} does not exist.`)
|
||||
else {
|
||||
log(`Channel/Thread ${message.channel}-${message.author.username} does not exist. File will be created shortly...`)
|
||||
reject(new Error(`Failed to find ${message.author.username}'s history. Try chatting again.`))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
getThread(`${message.channelId}.json`, (threadInfo) => {
|
||||
if (threadInfo?.messages)
|
||||
resolve(threadInfo.messages)
|
||||
else
|
||||
log(`Thread ${message.channelId} does not exist.`)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// response string for ollama to put its response
|
||||
let response: string
|
||||
|
||||
// get message attachment if exists
|
||||
const messageAttachment: string[] = await getAttachmentData(message.attachments.first())
|
||||
const model: string = userConfig.options['switch-model']
|
||||
|
||||
// set up new queue
|
||||
msgHist.setQueue(chatMessages)
|
||||
|
||||
@@ -114,14 +119,15 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
|
||||
// push user response before ollama query
|
||||
msgHist.enqueue({
|
||||
role: 'user',
|
||||
content: clean(message.content)
|
||||
content: cleanedMessage,
|
||||
images: messageAttachment || []
|
||||
})
|
||||
|
||||
// undefined or false, use normal, otherwise use embed
|
||||
if (userConfig.options['message-style'])
|
||||
response = await embedMessage(message, ollama, tokens, msgHist, shouldStream)
|
||||
response = await embedMessage(message, ollama, model, msgHist, shouldStream)
|
||||
else
|
||||
response = await normalMessage(message, ollama, tokens, msgHist, shouldStream)
|
||||
response = await normalMessage(message, ollama, model, msgHist, shouldStream)
|
||||
|
||||
// If something bad happened, remove user query and stop
|
||||
if (response == undefined) { msgHist.pop(); return }
|
||||
@@ -132,22 +138,16 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
|
||||
// successful query, save it in context history
|
||||
msgHist.enqueue({
|
||||
role: 'assistant',
|
||||
content: response
|
||||
content: response,
|
||||
images: messageAttachment || []
|
||||
})
|
||||
|
||||
// only update the json on success
|
||||
if (serverConfig.options['channel-toggle']) {
|
||||
openChannelInfo(message.channelId,
|
||||
message.channel as TextChannel,
|
||||
message.author.tag,
|
||||
msgHist.getItems()
|
||||
)
|
||||
} else {
|
||||
openThreadInfo(`${message.channelId}.json`,
|
||||
client.channels.fetch(message.channelId) as unknown as ThreadChannel,
|
||||
msgHist.getItems()
|
||||
)
|
||||
}
|
||||
openChannelInfo(message.channelId,
|
||||
message.channel as TextChannel,
|
||||
message.author.tag,
|
||||
msgHist.getItems()
|
||||
)
|
||||
} catch (error: any) {
|
||||
msgHist.pop() // remove message because of failure
|
||||
message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { event, Events, registerCommands } from '../utils/index.js'
|
||||
import { ActivityType } from 'discord.js'
|
||||
import { event, Events, registerCommands } from '../utils/index.js'
|
||||
import commands from '../commands/index.js'
|
||||
|
||||
// Log when the bot successfully logs in and export it
|
||||
|
||||
@@ -5,16 +5,36 @@ import fs from 'fs'
|
||||
/**
|
||||
* Event to remove the associated .json file for a thread once deleted
|
||||
*/
|
||||
export default event(Events.ThreadDelete, ({ log }, thread: ThreadChannel) => {
|
||||
const filePath = `data/${thread.id}.json`
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlink(filePath, (error) => {
|
||||
if (error)
|
||||
log(`Error deleting file ${filePath}`, error)
|
||||
else
|
||||
log(`Successfully deleted ${filePath} thread info`)
|
||||
export default event(Events.ThreadDelete, async ({ log }, thread: ThreadChannel) => {
|
||||
// iterate through every guild member in the thread and delete their history, except the bot
|
||||
try {
|
||||
log(`Number of User Guild Members in Thread being deleted: ${thread.memberCount!! - 1}`)
|
||||
const dirPath = 'data/'
|
||||
|
||||
// read all files in data/
|
||||
fs.readdir(dirPath, (error, files) => {
|
||||
if (error) {
|
||||
log(`Error reading directory ${dirPath}`, error)
|
||||
return
|
||||
}
|
||||
|
||||
// filter files by thread id being deleted
|
||||
const filesToDiscard = files.filter(
|
||||
file => file.startsWith(`${thread.id}-`) &&
|
||||
file.endsWith('.json'))
|
||||
|
||||
// remove files by unlinking
|
||||
filesToDiscard.forEach(file => {
|
||||
const filePath = dirPath + file
|
||||
fs.unlink(filePath, error => {
|
||||
if (error)
|
||||
log(`Error deleting file ${filePath}`, error)
|
||||
else
|
||||
log(`Successfully deleted ${filePath} thread information`)
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
log(`File ${filePath} does not exist.`)
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Issue deleting user history files from ${thread.id}`)
|
||||
}
|
||||
})
|
||||
@@ -1,12 +1,9 @@
|
||||
import { getEnvVar } from './utils/env.js'
|
||||
import { getEnvVar } from './utils/index.js'
|
||||
|
||||
export const Keys = {
|
||||
clientToken: getEnvVar('CLIENT_TOKEN'),
|
||||
model: getEnvVar('MODEL'),
|
||||
clientUid: getEnvVar('CLIENT_UID'),
|
||||
guildId: getEnvVar('GUILD_ID'),
|
||||
ipAddress: getEnvVar('OLLAMA_IP'),
|
||||
portAddress: getEnvVar('OLLAMA_PORT'),
|
||||
ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none
|
||||
portAddress: getEnvVar('OLLAMA_PORT', '11434'), // default ollama port if none
|
||||
} as const // readonly keys
|
||||
|
||||
export default Keys
|
||||
68
src/utils/configInterfaces.ts
Normal file
68
src/utils/configInterfaces.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ChannelType } from 'discord.js'
|
||||
import { UserMessage } from './index.js'
|
||||
|
||||
export interface UserConfiguration {
|
||||
'message-stream'?: boolean,
|
||||
'message-style'?: boolean,
|
||||
'modify-capacity': number,
|
||||
'switch-model': string
|
||||
}
|
||||
|
||||
export interface ServerConfiguration {
|
||||
'toggle-chat'?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent Configuration interface
|
||||
*
|
||||
* @see ServerConfiguration server settings per guild
|
||||
* @see UserConfiguration user configurations (only for the user for any server)
|
||||
*/
|
||||
export interface Configuration {
|
||||
readonly name: string
|
||||
options: UserConfiguration | ServerConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* User config to use outside of this file
|
||||
*/
|
||||
export interface UserConfig {
|
||||
readonly name: string
|
||||
options: UserConfiguration
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
readonly name: string
|
||||
options: ServerConfiguration
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly user: string
|
||||
messages: UserMessage[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The following 2 types is allow for better readability in commands
|
||||
* Admin Command -> Don't run in Threads
|
||||
* User Command -> Used anywhere
|
||||
*/
|
||||
export const AdminCommand = [
|
||||
ChannelType.GuildText
|
||||
]
|
||||
|
||||
export const UserCommand = [
|
||||
ChannelType.GuildText,
|
||||
ChannelType.PublicThread,
|
||||
ChannelType.PrivateThread
|
||||
]
|
||||
|
||||
/**
|
||||
* Check if the configuration we are editing/taking from is a Server Config
|
||||
* @param key name of command we ran
|
||||
* @returns true if command is from Server Config, false otherwise
|
||||
*/
|
||||
export function isServerConfigurationKey(key: string): key is keyof ServerConfiguration {
|
||||
return ['toggle-chat'].includes(key);
|
||||
}
|
||||
@@ -1,21 +1,35 @@
|
||||
import { resolve } from 'path'
|
||||
import { config } from 'dotenv'
|
||||
|
||||
// Find config - ONLY WORKS WITH NODEMON
|
||||
const envFile = process.env.NODE_ENV === 'development' ? '.env.dev.local' : '.env'
|
||||
|
||||
// resolve config file
|
||||
const envFilePath = resolve(process.cwd(), envFile)
|
||||
const envFilePath = resolve(process.cwd(), '.env')
|
||||
const ipValidate: RegExp = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/
|
||||
|
||||
// set current environment variable file
|
||||
config({ path: envFilePath })
|
||||
|
||||
// Getter for environment variables
|
||||
/**
|
||||
* Method to validate if environment variables found in file utils/env.ts
|
||||
*
|
||||
* @param name Name of the environment variable in .env
|
||||
* @param fallback fallback value to set if environment variable is not set (used manually in src/keys.ts)
|
||||
* @returns environment variable value
|
||||
*/
|
||||
export function getEnvVar(name: string, fallback?: string): string {
|
||||
const value = process.env[name] ?? fallback
|
||||
if (value == undefined)
|
||||
if (!value)
|
||||
throw new Error(`Environment variable ${name} is not set.`)
|
||||
|
||||
// validate User-Generated Discord Application Tokens
|
||||
if (name === "CLIENT_TOKEN")
|
||||
if (value.length < 72) throw new Error(`The "CLIENT_TOKEN" provided is not of at least length 72.
|
||||
This is probably an invalid token unless Discord updated their token policy. Please provide a valid token.`)
|
||||
|
||||
// validate IPv4 address found in environment variables
|
||||
if (name.endsWith("_IP") || name.endsWith("_ADDRESS"))
|
||||
if (!ipValidate.test(value))
|
||||
throw new Error(`Environment variable ${name} does not follow IPv4 formatting.`)
|
||||
|
||||
// return env variable
|
||||
return value
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClientEvents, Awaitable, Client, User } from 'discord.js'
|
||||
import type { ClientEvents, Awaitable, Client } from 'discord.js'
|
||||
import { Ollama } from 'ollama'
|
||||
import { Queue } from '../queues/queue.js'
|
||||
|
||||
@@ -8,17 +8,6 @@ export { Events } from 'discord.js'
|
||||
export type LogMethod = (...args: unknown[]) => void
|
||||
export type EventKeys = keyof ClientEvents // only wants keys of ClientEvents object
|
||||
|
||||
/**
|
||||
* Tokens to run the bot as intended
|
||||
* @param channel the channel where the bot will respond to queries
|
||||
* @param model chosen model for the ollama to utilize
|
||||
* @param clientUid the discord id for the bot
|
||||
*/
|
||||
export type Tokens = {
|
||||
model: string,
|
||||
clientUid: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters to run the chat query
|
||||
* @param model the model to run
|
||||
@@ -38,7 +27,8 @@ export type ChatParams = {
|
||||
*/
|
||||
export type UserMessage = {
|
||||
role: string,
|
||||
content: string
|
||||
content: string,
|
||||
images: string[] // May or may not have images in message
|
||||
}
|
||||
|
||||
// Event properties
|
||||
@@ -46,7 +36,6 @@ export interface EventProps {
|
||||
client: Client
|
||||
log: LogMethod
|
||||
msgHist: Queue<UserMessage>
|
||||
tokens: Tokens,
|
||||
ollama: Ollama
|
||||
}
|
||||
export type EventCallback<T extends EventKeys> = (
|
||||
@@ -69,14 +58,12 @@ export function event<T extends EventKeys>(key: T, callback: EventCallback<T>):
|
||||
* @param client initialized bot client
|
||||
* @param events all the exported events from the index.ts in the events dir
|
||||
* @param msgHist The message history of the bot
|
||||
* @param tokens the passed in environment tokens for the service
|
||||
* @param ollama the initialized ollama instance
|
||||
*/
|
||||
export function registerEvents(
|
||||
client: Client,
|
||||
events: Event[],
|
||||
msgHist: Queue<UserMessage>,
|
||||
tokens: Tokens,
|
||||
ollama: Ollama
|
||||
): void {
|
||||
for (const { key, callback } of events) {
|
||||
@@ -86,7 +73,7 @@ export function registerEvents(
|
||||
|
||||
// Handle Errors, call callback, log errors as needed
|
||||
try {
|
||||
callback({ client, log, msgHist, tokens, ollama }, ...args)
|
||||
callback({ client, log, msgHist, ollama }, ...args)
|
||||
} catch (error) {
|
||||
log('[Uncaught Error]', error)
|
||||
}
|
||||
|
||||
57
src/utils/handlers/bufferHandler.ts
Normal file
57
src/utils/handlers/bufferHandler.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Attachment } from "discord.js"
|
||||
|
||||
/**
|
||||
* Method to convert a Discord attachment url to an array buffer
|
||||
*
|
||||
* @param url Discord Attachment Url
|
||||
* @returns array buffer from Attachment Url
|
||||
*/
|
||||
async function getAttachmentBuffer(url: string): Promise<ArrayBuffer> {
|
||||
// Get the data from the image
|
||||
const response = await fetch(url)
|
||||
|
||||
// Validate the image came in fine
|
||||
if (!response.ok)
|
||||
throw new Error('Failed to fetch the attachment.')
|
||||
|
||||
// Return image as Buffer
|
||||
return await response.arrayBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to convert an array buffer to a Base64 String
|
||||
*
|
||||
* @param buffer Array Buffer from attachment
|
||||
* @returns converted Base64 string
|
||||
*/
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
// Converting to Uint8Array
|
||||
const uint8Array = new Uint8Array(buffer)
|
||||
let binary = ''
|
||||
const len = uint8Array.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(uint8Array[i])
|
||||
}
|
||||
|
||||
// Return as Base64
|
||||
return Buffer.from(binary, 'binary').toString('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve the Base64 Array of provided Message Attachment
|
||||
*
|
||||
* @param attachment Message Attachment from Discord
|
||||
* @returns Base64 string array
|
||||
*/
|
||||
export async function getAttachmentData(attachment: Attachment | undefined): Promise<string[]> {
|
||||
const url: string = attachment !== undefined ? attachment.url : "Missing Url"
|
||||
|
||||
// case of no attachment
|
||||
if (url === "Missing Url")
|
||||
return []
|
||||
|
||||
// Convert data to base64
|
||||
const buffer = await getAttachmentBuffer(url)
|
||||
const base64String = arrayBufferToBase64(buffer)
|
||||
return [base64String]
|
||||
}
|
||||
114
src/utils/handlers/chatHistoryHandler.ts
Normal file
114
src/utils/handlers/chatHistoryHandler.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { TextChannel, ThreadChannel } from 'discord.js'
|
||||
import { Configuration, Channel, UserMessage } from '../index.js'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Method to check if a thread history file exists
|
||||
*
|
||||
* @param channel parent thread of the requested thread (can be GuildText)
|
||||
* @returns true if channel does not exist, false otherwise
|
||||
*/
|
||||
async function checkChannelInfoExists(channel: TextChannel, user: string) {
|
||||
const doesExists: boolean = await new Promise((resolve) => {
|
||||
getChannelInfo(`${channel.id}-${user}.json`, (channelInfo) => {
|
||||
if (channelInfo?.messages)
|
||||
resolve(true)
|
||||
else
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
return doesExists
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to clear channel history for requesting user
|
||||
*
|
||||
* @param filename guild id string
|
||||
* @param channel the TextChannel in the Guild
|
||||
* @param user username of user
|
||||
* @returns nothing
|
||||
*/
|
||||
export async function clearChannelInfo(filename: string, channel: TextChannel, user: string): Promise<boolean> {
|
||||
const channelInfoExists: boolean = await checkChannelInfoExists(channel, user)
|
||||
|
||||
// If thread does not exist, file can't be found
|
||||
if (!channelInfoExists) return false
|
||||
|
||||
// Attempt to clear user channel history
|
||||
const fullFileName = `data/${filename}-${user}.json`
|
||||
const cleanedHistory: boolean = await new Promise((resolve) => {
|
||||
fs.readFile(fullFileName, 'utf8', (error, data) => {
|
||||
if (error)
|
||||
console.log(`[Error: openChannelInfo] Incorrect file format`)
|
||||
else {
|
||||
const object = JSON.parse(data)
|
||||
if (object['messages'].length === 0) // already empty, let user know
|
||||
resolve(false)
|
||||
else {
|
||||
object['messages'] = [] // cleared history
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return cleanedHistory
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to open the channel history
|
||||
*
|
||||
* @param filename name of the json file for the channel by user
|
||||
* @param channel the text channel info
|
||||
* @param user the user's name
|
||||
* @param messages their messages
|
||||
*/
|
||||
export async function openChannelInfo(filename: string, channel: TextChannel | ThreadChannel, user: string, messages: UserMessage[] = []): Promise<void> {
|
||||
const fullFileName = `data/${filename}-${user}.json`
|
||||
if (fs.existsSync(fullFileName)) {
|
||||
fs.readFile(fullFileName, 'utf8', (error, data) => {
|
||||
if (error)
|
||||
console.log(`[Error: openChannelInfo] 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 { // file doesn't exist, create it
|
||||
const object: Configuration = JSON.parse(`{ \"id\": \"${channel?.id}\", \"name\": \"${channel?.name}\", \"user\": \"${user}\", \"messages\": []}`)
|
||||
|
||||
const directory = path.dirname(fullFileName)
|
||||
if (!fs.existsSync(directory))
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
|
||||
// only creating it, no need to add anything
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
console.log(`[Util: openChannelInfo] Created '${fullFileName}' in working directory`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the channel information/history
|
||||
*
|
||||
* @param filename name of the json file for the channel by user
|
||||
* @param callback function to handle resolving message history
|
||||
*/
|
||||
export async function getChannelInfo(filename: string, callback: (config: Channel | undefined) => void): Promise<void> {
|
||||
const fullFileName = `data/${filename}`
|
||||
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
|
||||
}
|
||||
}
|
||||
92
src/utils/handlers/configHandler.ts
Normal file
92
src/utils/handlers/configHandler.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Configuration, ServerConfig, UserConfig, isServerConfigurationKey } from '../index.js'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Method to open a file in the working directory and modify/create it
|
||||
*
|
||||
* @param filename name of the file
|
||||
* @param key key value to access
|
||||
* @param value new value to assign
|
||||
*/
|
||||
// add type of change (server, user)
|
||||
export function openConfig(filename: string, key: string, value: any) {
|
||||
const fullFileName = `data/${filename}`
|
||||
|
||||
// check if the file exists, if not then make the config file
|
||||
if (fs.existsSync(fullFileName)) {
|
||||
fs.readFile(fullFileName, 'utf8', (error, data) => {
|
||||
if (error)
|
||||
console.log(`[Error: openConfig] Incorrect file format`)
|
||||
else {
|
||||
const object = JSON.parse(data)
|
||||
object['options'][key] = value
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
}
|
||||
})
|
||||
} else { // work on dynamic file creation
|
||||
let object: Configuration
|
||||
if (isServerConfigurationKey(key))
|
||||
object = JSON.parse('{ \"name\": \"Server Confirgurations\" }')
|
||||
else
|
||||
object = JSON.parse('{ \"name\": \"User Confirgurations\" }')
|
||||
|
||||
// set standard information for config file and options
|
||||
object['options'] = {
|
||||
[key]: value
|
||||
}
|
||||
|
||||
const directory = path.dirname(fullFileName)
|
||||
if (!fs.existsSync(directory))
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
|
||||
fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2))
|
||||
console.log(`[Util: openConfig] Created '${filename}' in working directory`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to obtain the configurations of the message chat/thread
|
||||
*
|
||||
* @param filename name of the configuration file to get
|
||||
* @param callback function to allow a promise from getting the config
|
||||
*/
|
||||
export async function getServerConfig(filename: string, callback: (config: ServerConfig | undefined) => void): Promise<void> {
|
||||
const fullFileName = `data/${filename}`
|
||||
|
||||
// attempt to read the file and get the configuration
|
||||
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 obtain the configurations of the message chat/thread
|
||||
*
|
||||
* @param filename name of the configuration file to get
|
||||
* @param callback function to allow a promise from getting the config
|
||||
*/
|
||||
export async function getUserConfig(filename: string, callback: (config: UserConfig | undefined) => void): Promise<void> {
|
||||
const fullFileName = `data/${filename}`
|
||||
|
||||
// attempt to read the file and get the configuration
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChatResponse } from "ollama"
|
||||
import { ChatParams } from "./index.js"
|
||||
import { ChatParams } from "../index.js"
|
||||
import { AbortableAsyncIterator } from "ollama/src/utils.js"
|
||||
|
||||
/**
|
||||
@@ -4,4 +4,11 @@ export * from './events.js'
|
||||
export * from './messageEmbed.js'
|
||||
export * from './messageNormal.js'
|
||||
export * from './commands.js'
|
||||
export * from './streamHandler.js'
|
||||
export * from './configInterfaces.js'
|
||||
export * from './mentionClean.js'
|
||||
|
||||
// handler imports
|
||||
export * from './handlers/chatHistoryHandler.js'
|
||||
export * from './handlers/configHandler.js'
|
||||
export * from './handlers/streamHandler.js'
|
||||
export * from './handlers/bufferHandler.js'
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import { TextChannel, ThreadChannel } from 'discord.js'
|
||||
import { UserMessage } from './events.js'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export interface UserConfiguration {
|
||||
'message-stream'?: boolean,
|
||||
'message-style'?: boolean,
|
||||
'modify-capacity': number
|
||||
}
|
||||
|
||||
export interface ServerConfiguration {
|
||||
'toggle-chat'?: boolean,
|
||||
'channel-toggle'?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent Configuration interface
|
||||
*
|
||||
* @see ServerConfiguration server settings per guild
|
||||
* @see UserConfiguration user configurations (only for the user for any server)
|
||||
*/
|
||||
export interface Configuration {
|
||||
readonly name: string
|
||||
options: UserConfiguration | ServerConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* User config to use outside of this file
|
||||
*/
|
||||
export interface UserConfig {
|
||||
readonly name: string
|
||||
options: UserConfiguration
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
readonly name: string
|
||||
options: ServerConfiguration
|
||||
}
|
||||
|
||||
export interface Thread {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
messages: UserMessage[]
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly user: string
|
||||
messages: UserMessage[]
|
||||
}
|
||||
|
||||
function isUserConfigurationKey(key: string): key is keyof UserConfiguration {
|
||||
return ['message-stream', 'message-style', 'modify-capacity'].includes(key);
|
||||
}
|
||||
|
||||
function isServerConfigurationKey(key: string): key is keyof ServerConfiguration {
|
||||
return ['toggle-chat', 'channel-toggle'].includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to open a file in the working directory and modify/create it
|
||||
*
|
||||
* @param filename name of the file
|
||||
* @param key key value to access
|
||||
* @param value new value to assign
|
||||
*/
|
||||
// add type of change (server, user)
|
||||
export function openConfig(filename: string, key: string, value: any) {
|
||||
const fullFileName = `data/${filename}`
|
||||
|
||||
// check if the file exists, if not then make the config file
|
||||
if (fs.existsSync(fullFileName)) {
|
||||
fs.readFile(fullFileName, 'utf8', (error, data) => {
|
||||
if (error)
|
||||
console.log(`[Error: openConfig] Incorrect file format`)
|
||||
else {
|
||||
const object = JSON.parse(data)
|
||||
object['options'][key] = value
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
}
|
||||
})
|
||||
} else { // work on dynamic file creation
|
||||
let object: Configuration
|
||||
if (isServerConfigurationKey(key))
|
||||
object = JSON.parse('{ \"name\": \"Server Confirgurations\" }')
|
||||
else
|
||||
object = JSON.parse('{ \"name\": \"User Confirgurations\" }')
|
||||
|
||||
// set standard information for config file and options
|
||||
object['options'] = {
|
||||
[key]: value
|
||||
}
|
||||
|
||||
fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2))
|
||||
console.log(`[Util: openConfig] Created '${filename}' in working directory`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to obtain the configurations of the message chat/thread
|
||||
*
|
||||
* @param filename name of the configuration file to get
|
||||
* @param callback function to allow a promise from getting the config
|
||||
*/
|
||||
export async function getServerConfig(filename: string, callback: (config: ServerConfig | undefined) => void): Promise<void> {
|
||||
const fullFileName = `data/${filename}`
|
||||
|
||||
// attempt to read the file and get the configuration
|
||||
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 obtain the configurations of the message chat/thread
|
||||
*
|
||||
* @param filename name of the configuration file to get
|
||||
* @param callback function to allow a promise from getting the config
|
||||
*/
|
||||
export async function getUserConfig(filename: string, callback: (config: UserConfig | undefined) => void): Promise<void> {
|
||||
const fullFileName = `data/${filename}`
|
||||
|
||||
// attempt to read the file and get the configuration
|
||||
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/create and modify a json file containing thread information
|
||||
*
|
||||
* @param filename name of the thread file
|
||||
* @param thread the thread with all of the interactions
|
||||
* @param message message contents and from who
|
||||
*/
|
||||
export function openThreadInfo(filename: string, thread: ThreadChannel, messages: UserMessage[] = []) {
|
||||
// check if the file exists, if not then make the config file
|
||||
const fullFileName = `data/${filename}`
|
||||
if (fs.existsSync(fullFileName)) {
|
||||
fs.readFile(fullFileName, 'utf8', (error, data) => {
|
||||
if (error)
|
||||
console.log(`[Error: openThreadInfo] Incorrect file format`)
|
||||
else {
|
||||
const object = JSON.parse(data)
|
||||
object['messages'] = messages as []
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
}
|
||||
})
|
||||
} else { // file doesn't exist, create it
|
||||
const object: Configuration = JSON.parse(`{ \"id\": \"${thread?.id}\", \"name\": \"${thread?.name}\", \"messages\": []}`)
|
||||
|
||||
const directory = path.dirname(fullFileName)
|
||||
if (!fs.existsSync(directory))
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
|
||||
// only creating it, no need to add anything
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
console.log(`[Util: openThreadInfo] Created '${fullFileName}' in working directory`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to obtain the configurations of the message chat/thread
|
||||
*
|
||||
* @param filename name of the configuration file to get
|
||||
* @param callback function to allow a promise from getting the config
|
||||
*/
|
||||
export async function getThread(filename: string, callback: (config: Thread | undefined) => void): Promise<void> {
|
||||
// attempt to read the file and get the configuration
|
||||
const fullFileName = `data/${filename}`
|
||||
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
|
||||
*
|
||||
* @param filename name of the json file for the channel by user
|
||||
* @param channel the text channel info
|
||||
* @param user the user's name
|
||||
* @param messages their messages
|
||||
*/
|
||||
export async function openChannelInfo(filename: string, channel: TextChannel, user: string, messages: UserMessage[] = []): Promise<void> {
|
||||
// thread exist handler
|
||||
const isThread: boolean = await new Promise((resolve) => {
|
||||
getThread(`${channel.id}.json`, (threadInfo) => {
|
||||
if (threadInfo?.messages)
|
||||
resolve(true)
|
||||
else
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
|
||||
// This is an existing thread, don't create another json
|
||||
if (isThread) return
|
||||
|
||||
const fullFileName = `data/${filename}-${user}.json`
|
||||
if (fs.existsSync(fullFileName)) {
|
||||
fs.readFile(fullFileName, 'utf8', (error, data) => {
|
||||
if (error)
|
||||
console.log(`[Error: openChannelInfo] 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 { // file doesn't exist, create it
|
||||
const object: Configuration = JSON.parse(`{ \"id\": \"${channel?.id}\", \"name\": \"${channel?.name}\", \"user\": \"${user}\", \"messages\": []}`)
|
||||
|
||||
const directory = path.dirname(fullFileName)
|
||||
if (!fs.existsSync(directory))
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
|
||||
// only creating it, no need to add anything
|
||||
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
|
||||
console.log(`[Util: openChannelInfo] Created '${fullFileName}' in working directory`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the channel information/history
|
||||
*
|
||||
* @param filename name of the json file for the channel by user
|
||||
* @param callback function to handle resolving message history
|
||||
*/
|
||||
export async function getChannelInfo(filename: string, callback: (config: Channel | undefined) => void): Promise<void> {
|
||||
const fullFileName = `data/${filename}`
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import Keys from "../keys.js"
|
||||
* - replace function works well for this
|
||||
*
|
||||
* @param message
|
||||
* @returns
|
||||
* @returns message without client id
|
||||
*/
|
||||
export function clean(message: string): string {
|
||||
const cleanedMessage: string = message.replace(`<@${Keys.clientUid}>`, '').trim()
|
||||
export function clean(message: string, clientId: string): string {
|
||||
const cleanedMessage: string = message.replace(`<@${clientId}>`, '').trim()
|
||||
return cleanedMessage
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EmbedBuilder, Message } from 'discord.js'
|
||||
import { EmbedBuilder, Message, SendableChannels } from 'discord.js'
|
||||
import { ChatResponse, Ollama } from 'ollama'
|
||||
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
|
||||
import { Queue } from '../queues/queue.js'
|
||||
@@ -7,15 +7,13 @@ import { AbortableAsyncIterator } from 'ollama/src/utils.js'
|
||||
/**
|
||||
* Method to send replies as normal text on discord like any other user
|
||||
* @param message message sent by the user
|
||||
* @param tokens tokens to run query
|
||||
* @param model name of model to run query
|
||||
* @param msgHist message history between user and model
|
||||
*/
|
||||
export async function embedMessage(
|
||||
message: Message,
|
||||
ollama: Ollama,
|
||||
tokens: {
|
||||
model: string
|
||||
},
|
||||
model: string,
|
||||
msgHist: Queue<UserMessage>,
|
||||
stream: boolean
|
||||
): Promise<string> {
|
||||
@@ -30,11 +28,12 @@ export async function embedMessage(
|
||||
.setColor('#00FF00')
|
||||
|
||||
// send the message
|
||||
const sentMessage = await message.channel.send({ embeds: [botMessage] })
|
||||
const channel = message.channel as SendableChannels
|
||||
const sentMessage = await channel.send({ embeds: [botMessage] })
|
||||
|
||||
// create params
|
||||
const params: ChatParams = {
|
||||
model: tokens.model,
|
||||
model: model,
|
||||
ollama: ollama,
|
||||
msgHist: msgHist.getItems()
|
||||
}
|
||||
@@ -50,12 +49,12 @@ export async function embedMessage(
|
||||
// exceeds handled length
|
||||
if (result.length > 5000) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle(`Responding to ${message.author.tag}`)
|
||||
.setDescription(`Response length ${result.length} has exceeded Discord maximum.\n\nLong Stream messages not supported.`)
|
||||
.setColor('#00FF00')
|
||||
.setTitle(`Responding to ${message.author.tag}`)
|
||||
.setDescription(`Response length ${result.length} has exceeded Discord maximum.\n\nLong Stream messages not supported.`)
|
||||
.setColor('#00FF00')
|
||||
|
||||
// send error
|
||||
message.channel.send({ embeds: [errorEmbed] })
|
||||
channel.send({ embeds: [errorEmbed] })
|
||||
break // cancel loop and stop
|
||||
}
|
||||
|
||||
@@ -92,7 +91,7 @@ export async function embedMessage(
|
||||
.setDescription(result.slice(0, 5000) || 'No Content to Provide...')
|
||||
.setColor('#00FF00')
|
||||
|
||||
message.channel.send({ embeds: [whileEmbed] })
|
||||
channel.send({ embeds: [whileEmbed] })
|
||||
result = result.slice(5000)
|
||||
}
|
||||
|
||||
@@ -102,7 +101,7 @@ export async function embedMessage(
|
||||
.setColor('#00FF00')
|
||||
|
||||
// rest of the response
|
||||
message.channel.send({ embeds: [lastEmbed] })
|
||||
channel.send({ embeds: [lastEmbed] })
|
||||
} else {
|
||||
// only need to create 1 embed, handles 6000 characters
|
||||
const newEmbed = new EmbedBuilder()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Message } from 'discord.js'
|
||||
import { Message, SendableChannels } from 'discord.js'
|
||||
import { ChatResponse, Ollama } from 'ollama'
|
||||
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
|
||||
import { Queue } from '../queues/queue.js'
|
||||
@@ -7,26 +7,25 @@ import { AbortableAsyncIterator } from 'ollama/src/utils.js'
|
||||
/**
|
||||
* Method to send replies as normal text on discord like any other user
|
||||
* @param message message sent by the user
|
||||
* @param tokens tokens to run query
|
||||
* @param model name of model to run query
|
||||
* @param msgHist message history between user and model
|
||||
*/
|
||||
export async function normalMessage(
|
||||
message: Message,
|
||||
ollama: Ollama,
|
||||
tokens: {
|
||||
model: string
|
||||
},
|
||||
model: string,
|
||||
msgHist: Queue<UserMessage>,
|
||||
stream: boolean
|
||||
): Promise<string> {
|
||||
// bot's respnse
|
||||
let response: ChatResponse | AbortableAsyncIterator<ChatResponse>
|
||||
let result: string = ''
|
||||
const channel = message.channel as SendableChannels
|
||||
|
||||
await message.channel.send('Generating Response . . .').then(async sentMessage => {
|
||||
await channel.send('Generating Response . . .').then(async sentMessage => {
|
||||
try {
|
||||
const params: ChatParams = {
|
||||
model: tokens.model,
|
||||
model: model,
|
||||
ollama: ollama,
|
||||
msgHist: msgHist.getItems()
|
||||
}
|
||||
@@ -41,7 +40,7 @@ export async function normalMessage(
|
||||
result = portion.message.content
|
||||
|
||||
// new message block, wait for it to send and assign new block to respond.
|
||||
await message.channel.send("Creating new stream block...").then(sentMessage => { messageBlock = sentMessage })
|
||||
await channel.send("Creating new stream block...").then(sentMessage => { messageBlock = sentMessage })
|
||||
} else {
|
||||
result += portion.message.content
|
||||
|
||||
@@ -63,12 +62,12 @@ export async function normalMessage(
|
||||
|
||||
// handle for rest of message that is >2000
|
||||
while (result.length > 2000) {
|
||||
message.channel.send(result.slice(0, 2000))
|
||||
channel.send(result.slice(0, 2000))
|
||||
result = result.slice(2000)
|
||||
}
|
||||
|
||||
// last part of message
|
||||
message.channel.send(result)
|
||||
channel.send(result)
|
||||
} else // edit the 'generic' response to new message since <2000
|
||||
sentMessage.edit(result)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// expect takes a value from an expression
|
||||
// it marks a test case
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import commands from '../src/commands'
|
||||
import commands from '../src/commands/index.js'
|
||||
|
||||
/**
|
||||
* Commands test suite, tests the commands object
|
||||
@@ -12,7 +12,7 @@ import commands from '../src/commands'
|
||||
* @param name name of the test suite
|
||||
* @param fn function holding tests to run
|
||||
*/
|
||||
describe('#commands', () => {
|
||||
describe('Commands Existence', () => {
|
||||
// test definition of commands object
|
||||
it('references defined object', () => {
|
||||
// toBe compares the value to the expected value
|
||||
@@ -22,6 +22,51 @@ describe('#commands', () => {
|
||||
// test specific commands in the object
|
||||
it('references specific commands', () => {
|
||||
const commandsString = commands.map(e => e.name).join(', ')
|
||||
expect(commandsString).toBe('thread, private-thread, message-style, message-stream, toggle-chat, shutoff, modify-capacity, channel-toggle')
|
||||
expect(commandsString).toBe('thread, private-thread, message-style, message-stream, toggle-chat, shutoff, modify-capacity, clear-user-channel-history, pull-model, switch-model')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* User Commands Test suite for testing out commands
|
||||
* that would be run by users when using the application.
|
||||
*/
|
||||
describe('User Command Tests', () => {
|
||||
// test capacity command
|
||||
it('run modify-capacity command', () => {
|
||||
|
||||
})
|
||||
|
||||
it('run clear-user-channel-history command', () => {
|
||||
|
||||
})
|
||||
|
||||
it('run message-stream command', () => {
|
||||
|
||||
})
|
||||
|
||||
it('run message-style command', () => {
|
||||
|
||||
})
|
||||
|
||||
it('run thread command', () => {
|
||||
|
||||
})
|
||||
|
||||
it('run private-thread command', () => {
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Admin Commands Test suite for running administrative
|
||||
* commands with the application.
|
||||
*/
|
||||
describe('Admin Command Tests', () => {
|
||||
it('run shutoff command', () => {
|
||||
|
||||
})
|
||||
|
||||
it('run toggle-chat command', () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import events from '../src/events'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import events from '../src/events/index.js'
|
||||
|
||||
/**
|
||||
* Mocking ollama found in client.ts because pullModel.ts
|
||||
* relies on the existence on ollama. To prevent the mock,
|
||||
* we will have to pass through ollama to the commands somehow.
|
||||
*/
|
||||
vi.mock('../src/client.js', () => ({
|
||||
ollama: {
|
||||
pull: vi.fn() // Mock the pull method found with ollama
|
||||
}
|
||||
}))
|
||||
|
||||
/**
|
||||
* Events test suite, tests the events object
|
||||
* Each event is to be tested elsewhere, this file
|
||||
* is to ensure that the events object is defined.
|
||||
*
|
||||
* @param name name of the test suite
|
||||
* @param fn function holding tests to run
|
||||
*/
|
||||
describe('#events', () => {
|
||||
describe('Events Existence', () => {
|
||||
// test definition of events object
|
||||
it('references defined object', () => {
|
||||
expect(typeof events).toBe('object')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getEnvVar } from '../src/utils'
|
||||
import { getEnvVar } from '../src/utils/index.js'
|
||||
|
||||
/**
|
||||
* getEnvVar test suite, tests the getEnvVar function
|
||||
@@ -7,7 +7,7 @@ import { getEnvVar } from '../src/utils'
|
||||
* @param name name of the test suite
|
||||
* @param fn function holding tests to run
|
||||
*/
|
||||
describe('#getEnvVar', () => {
|
||||
describe('Environment Setup', () => {
|
||||
// dummy set of keys
|
||||
const keys = {
|
||||
clientToken: 'CLIENT_TOKEN',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { clean } from '../src/utils/mentionClean'
|
||||
import { getEnvVar } from '../src/utils'
|
||||
import { clean } from '../src/utils/index.js'
|
||||
|
||||
// Sample UID for testing
|
||||
const sampleId = '123456789'
|
||||
|
||||
/**
|
||||
* MentionClean test suite, tests the clean function
|
||||
@@ -8,10 +10,10 @@ import { getEnvVar } from '../src/utils'
|
||||
* @param name name of the test suite
|
||||
* @param fn function holding tests to run
|
||||
*/
|
||||
describe('#clean', () => {
|
||||
describe('Mentions Cleaned', () => {
|
||||
// test for id removal from message
|
||||
it('removes the mention from a message', () => {
|
||||
const message = `<@${getEnvVar('CLIENT_UID')}> Hello, World!`
|
||||
expect(clean(message)).toBe('Hello, World!')
|
||||
const message = `<@${sampleId}> Hello, World!`
|
||||
expect(clean(message, sampleId)).toBe('Hello, World!')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Queue } from '../src/queues/queue'
|
||||
import { Queue } from '../src/queues/queue.js'
|
||||
|
||||
/**
|
||||
* Queue test suite, tests the Queue class
|
||||
@@ -7,7 +7,7 @@ import { Queue } from '../src/queues/queue'
|
||||
* @param name name of the test suite
|
||||
* @param fn function holding tests to run
|
||||
*/
|
||||
describe('#queue', () => {
|
||||
describe('Queue Structure', () => {
|
||||
let queue= new Queue<string>()
|
||||
|
||||
// test for queue creation
|
||||
|
||||
@@ -4,9 +4,10 @@ import { defineConfig, configDefaults } from 'vitest/config'
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true, // <-- reduces test file imports
|
||||
reporters: ['verbose'], // <-- verbose output
|
||||
coverage: {
|
||||
exclude: [...configDefaults.exclude, 'build/*'], // <-- exclude JS build
|
||||
reporter: ['text', 'html'] // <-- reports in text, html
|
||||
exclude: [...configDefaults.exclude, 'build/*', 'tests/*'], // <-- exclude JS build
|
||||
reporter: ['text-summary'] // <-- report in text-summary
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user