Compare commits

..
10 Commits
Author SHA1 Message Date
JT2M0L3Y 50edd60ce5 Update: fix imports based on last pkg fix 2026-07-17 12:01:56 -07:00
JT2M0L3Y 0a902c2812 Added: defined objects directory 2026-07-17 12:01:26 -07:00
JT2M0L3Y e09a47bd43 Update: utility method logs use method name 2026-07-17 12:01:24 -07:00
Jonathan SmoleyandGitHub 3d6d651e18 drop refs to coverage (#197) 2026-07-17 11:30:46 -07:00
Jonathan SmoleyandGitHub 7583d6d179 Fix: Code Coverage Badge Failed (#196)
* switch coverage to codecov

* reset node, increment ver

* remove auto-pass job var

* remove coverage 'env' note

* set badge with new action

* add fqvn for action

* fix step name

* make badge commit explicit
2026-07-16 17:44:11 -07:00
Kevin DangandGitHub 4aadea4611 Summary Command (#194) 2026-06-02 15:34:48 -07:00
Kevin DangandGitHub c0e29b3bbe Pipeline Trigger Change and Newline Format (#193) 2026-05-11 11:42:48 -07:00
Kevin DangandGitHub 32137dacb0 Add: Filter out think tags in bot message (#190) 2025-10-04 20:17:26 -07:00
Kevin DangandGitHub c00ea5de98 Additional Channel Awareness (#186) 2025-07-31 19:04:51 -07:00
Kevin DangandGitHub b27cdfc162 Update Documentation with New Features (#185) 2025-06-22 20:43:50 -07:00
29 changed files with 975 additions and 1500 deletions
+2 -2
View File
@@ -8,10 +8,10 @@ assignees: ''
--- ---
## Issue ## Issue
A clear and concise description of what the problem/feature is. A clear and concise description of what the problem/feature is. PLease describe it as best as possible.
## Solution ## Solution
* Provide steps or ideals to how to implement or investigate this new feature. * Provide steps or ideas to how to implement or investigate this new feature.
## References ## References
* Provide additional context and external references here * Provide additional context and external references here
+77 -75
View File
@@ -1,75 +1,77 @@
name: Builds name: Builds
run-name: Validate Node and Docker Builds run-name: Validate Node and Docker Builds
on:
push: on:
branches: push:
- master # runs after Pull Request is merged branches:
- master # runs after Pull Request is merged
jobs:
Discord-Node-Build: # test if the node install and run jobs:
runs-on: ubuntu-latest Discord-Node-Build: # test if the node install and run
timeout-minutes: 2 runs-on: ubuntu-latest
steps: timeout-minutes: 2
- name: Checkout Repository
uses: actions/checkout@v4 steps:
- name: Checkout Repository
- name: Set up Node Environment lts/jod uses: actions/checkout@v4
uses: actions/setup-node@v4
with: - name: Set up Node Environment
node-version: lts/jod uses: actions/setup-node@v4
cache: "npm" with:
node-version: lts/jod
- name: Install Project Dependencies cache: npm
run: |
npm install - name: Install Project Dependencies
run: |
- name: Build Application npm install
run: |
npm run build - name: Build Application
run: |
- name: Create Environment Variables npm run build
run: |
touch .env - name: Create Environment Variables
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env run: |
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env touch .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
# set -e ensures if nohup fails, this section fails echo MODEL = ${{ secrets.MODEL }} >> .env
- name: Startup Discord Bot Client
run: | # set -e ensures if nohup fails, this section fails
set -e - name: Startup Discord Bot Client
nohup npm run prod & run: |
set -e
Discord-Ollama-Container-Build: # test docker build and run nohup npm run prod &
runs-on: ubuntu-latest
timeout-minutes: 2 Discord-Ollama-Container-Build: # test docker build and run
steps: runs-on: ubuntu-latest
- name: Checkout Repository timeout-minutes: 2
uses: actions/checkout@v4 steps:
- name: Checkout Repository
- name: Set up Node Environment lts/jod uses: actions/checkout@v4
uses: actions/setup-node@v4
with: - name: Set up Node Environment
node-version: lts/jod uses: actions/setup-node@v4
cache: "npm" with:
node-version: lts/jod
- name: Create Environment Variables cache: npm
run: |
touch .env - name: Create Environment Variables
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env run: |
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env touch .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Setup Docker Network and Images echo MODEL = ${{ secrets.MODEL }} >> .env
run: |
npm run docker:start-cpu - name: Setup Docker Network and Images
run: |
- name: Check Images Exist npm run docker:start-cpu
run: |
(docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama') || exit 1 - name: Check Images Exist
run: |
- name: Check Containers Exist (docker images | grep -q 'kevinthedang/discord-ollama' && docker images | grep -qE 'ollama/ollama') || exit 1
run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord') || exit 1 - name: Check Containers Exist
run: |
(docker ps | grep -q 'ollama' && docker ps | grep -q 'discord') || exit 1
-50
View File
@@ -1,50 +0,0 @@
name: Coverage
run-name: Code Coverage
on:
push:
branches:
- master
jobs:
Discord-Node-Coverage:
runs-on: ubuntu-latest
environment: coverage
timeout-minutes: 2
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node Environment lts/jod
uses: actions/setup-node@v4
with:
node-version: lts/jod
cache: "npm"
- name: Install Project Dependencies
run: |
npm install
- name: Create Environment Variables
run: |
touch .env
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env
- name: Collect Code Coverage
run: |
LINE_PCT=$(npm run coverage | tail -2 | head -1 | awk '{print $3}')
echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV
- name: Upload Code Coverage
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: ${{ vars.GIST_ID }}
filename: coverage.json
label: Coverage
message: ${{ env.COVERAGE }}
valColorRange: ${{ env.COVERAGE }}
maxColorRange: 100
minColorRange: 0
+4 -5
View File
@@ -1,15 +1,14 @@
name: Deploy name: Deploy
run-name: Deploy Application Latest run-name: Deploy Application Latest
on:
push: on: workflow_dispatch
tags:
- 'v*'
jobs: jobs:
Deploy-Application: Deploy-Application:
runs-on: self-hosted runs-on: self-hosted
environment: deploy environment: deploy
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -39,7 +38,7 @@ jobs:
git clone https://github.com/kevinthedang/discord-ollama.git ${{ secrets.PATH }} git clone https://github.com/kevinthedang/discord-ollama.git ${{ secrets.PATH }}
cd ${{ secrets.PATH }} cd ${{ secrets.PATH }}
- name: Install nvm and Node.js lts/jod - name: Install nvm and Node.js
run: | run: |
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
+50 -48
View File
@@ -1,48 +1,50 @@
name: Tests name: Tests
run-name: Unit Tests run-name: Unit Tests
on:
pull_request: on:
branches: pull_request:
- master branches:
paths: - master
- '*' paths:
- 'package*.json' - '*'
- 'src/**' - 'package*.json'
- 'tests/**' - 'src/**'
- '!docs/**' - 'tests/**'
- '!imgs/**' - '!docs/**'
- '!.github/**' - '!imgs/**'
- '.github/workflows/**' - '!.github/**'
- '!.gitignore' - '.github/workflows/**'
- '!LICENSE' - '!.gitignore'
- '!README' - '!LICENSE'
- '!README'
jobs:
Discord-Node-Test: jobs:
runs-on: ubuntu-latest Discord-Node-Test:
timeout-minutes: 2 runs-on: ubuntu-latest
steps: timeout-minutes: 2
- name: Checkout Repository
uses: actions/checkout@v4 steps:
- name: Checkout Repository
- name: Set up Node Environment lts/jod uses: actions/checkout@v4
uses: actions/setup-node@v4
with: - name: Set up Node Environment
node-version: lts/jod uses: actions/setup-node@v4
cache: "npm" with:
node-version: lts/jod
- name: Install Project Dependencies cache: npm
run: |
npm install - name: Install Project Dependencies
run: |
- name: Create Environment Variables npm install
run: |
touch .env - name: Create Environment Variables
echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env run: |
echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env touch .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo CLIENT_TOKEN = ${{ secrets.BOT_TOKEN }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo OLLAMA_IP = ${{ secrets.OLLAMA_IP }} >> .env
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
- name: Test Application echo MODEL = ${{ secrets.MODEL }} >> .env
run: |
npm run tests - name: Test Application
run: |
npm run tests
-10
View File
@@ -37,16 +37,6 @@ pids
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
+1 -1
View File
@@ -12,7 +12,7 @@
# Technical/Business Code Ownership # Technical/Business Code Ownership
/src/ @kevinthedang @JT2M0L3Y /src/ @kevinthedang @JT2M0L3Y
/tests/ @JT2M0L3Y /tests/ @JT2M0L3Y
/.github/ @kevinthedang /.github/ @kevinthedang @JT2M0L3Y
# Docker Ownership # Docker Ownership
Dockerfile @kevinthedang Dockerfile @kevinthedang
+20 -12
View File
@@ -4,28 +4,36 @@
<h3><a href="#"></a>Ollama as your Discord AI Assistant</h3> <h3><a href="#"></a>Ollama as your Discord AI Assistant</h3>
<p><a href="#"></a><a href="https://creativecommons.org/licenses/by/4.0/"><img alt="License" src="https://img.shields.io/badge/License-CC_BY_4.0-darkgreen.svg" /></a> <p><a href="#"></a><a href="https://creativecommons.org/licenses/by/4.0/"><img alt="License" src="https://img.shields.io/badge/License-CC_BY_4.0-darkgreen.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/kevinthedang/discord-ollama?logo=github" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml"><img alt="Build Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml"><img alt="Builds" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/build.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/deploy.yml"><img alt="Deploy Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/deploy.yml/badge.svg" /></a> <a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml"><img alt="Testing Status" src="https://github.com/kevinthedang/discord-ollama/actions/workflows/test.yml/badge.svg" /></a>
<a href="#"></a><a href="https://github.com/kevinthedang/discord-ollama/actions/workflows/coverage.yml"><img alt="Code Coverage" src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/kevinthedang/bc7b5dcfa16561ab02bb3df67a99b22d/raw/coverage.json"></a>
</div> </div>
## About/Goals ## About/Goals
Ollama is an AI model management tool that allows users to install and use custom large language models locally. Ollama is an AI model management tool that allows users to install and use custom large language models locally.
The project aims to: The project aims to:
* [x] Create a Discord bot that will utilize Ollama and chat to chat with users! * [x] Create a Discord bot that will utilize Ollama and chat to chat with users!
* [x] User Preferences on Chat * [x] User and Server Preferences
* [x] Message Persistance on Channels and Threads * [x] Message Persistance
* [x] Threads
* [x] Channels
* [x] Containerization with Docker * [x] Containerization with Docker
* [x] Slash Commands Compatible * [x] Slash Commands Compatible
* [x] Summary Command
* [ ] Model Info Command
* [ ] List Models Command
* [x] Pull Model Command
* [x] Switch Model Command
* [x] Delete Model Command
* [x] Create Thread Command
* [x] Create Private Thread Command
* [x] Message Stream Command
* [x] Change Message History Size Command
* [x] Clear Channel History Command (User Only)
* [x] Administrator Role Compatible
* [x] Generated Token Length Handling for >2000 * [x] Generated Token Length Handling for >2000
* [x] Token Length Handling of any message size * [x] Token Length Handling of any message size
* [x] User vs. Server Preferences * [x] Multi-User Chat Generation - This was built in from Ollama `v0.2.1+`
* [x] Administrator Role Compatible * [ ] Ollama Tool Support Implementation
* [x] Multi-User Chat Generation (Multiple users chatting at the same time) - This was built in from Ollama `v0.2.1+` * [ ] Enhanced Channel Context Awareness
* [x] Automatic and Manual model pulling through the Discord client * [ ] Improved User Replied Triggers
Further, Ollama provides the functionality to utilize custom models or provide context for the top-layer of any model available through the Ollama model library. Further, Ollama provides the functionality to utilize custom models or provide context for the top-layer of any model available through the Ollama model library.
* [Customize a model](https://github.com/ollama/ollama#customize-a-model) * [Customize a model](https://github.com/ollama/ollama#customize-a-model)
+1 -1
View File
@@ -7,7 +7,7 @@ services:
build: ./ # find docker file in designated path build: ./ # find docker file in designated path
container_name: discord container_name: discord
restart: always # rebuild container always restart: always # rebuild container always
image: kevinthedang/discord-ollama:0.8.6 image: kevinthedang/discord-ollama:0.9.1
environment: environment:
CLIENT_TOKEN: ${CLIENT_TOKEN} CLIENT_TOKEN: ${CLIENT_TOKEN}
OLLAMA_IP: ${OLLAMA_IP} OLLAMA_IP: ${OLLAMA_IP}
+422 -1128
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -1,12 +1,11 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.8.6", "version": "0.9.1",
"description": "Ollama Integration into discord", "description": "Ollama Integration into discord",
"main": "build/index.js", "main": "build/index.js",
"exports": "./build/index.js", "exports": "./build/index.js",
"scripts": { "scripts": {
"tests": "vitest run", "tests": "vitest run",
"coverage": "vitest run --coverage",
"watch": "tsx watch src", "watch": "tsx watch src",
"build": "tsc", "build": "tsc",
"prod": "node .", "prod": "node .",
@@ -32,7 +31,6 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@vitest/coverage-v8": "^3.0.9",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.3", "tsx": "^4.19.3",
"typescript": "^5.8.2", "typescript": "^5.8.2",
+43 -40
View File
@@ -1,41 +1,44 @@
import { Client, GatewayIntentBits } from 'discord.js' import { Client, GatewayIntentBits } from 'discord.js'
import { Ollama } from 'ollama' import { Ollama } from 'ollama'
import { Queue } from './queues/queue.js' import { Queue } from './components/index.js'
import { UserMessage, registerEvents } from './utils/index.js' import { UserMessage, registerEvents } from './utils/index.js'
import Events from './events/index.js' import Events from './events/index.js'
import Keys from './keys.js' import Keys from './keys.js'
// initialize the client with the following permissions when logging in // initialize the client with the following permissions when logging in
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent GatewayIntentBits.MessageContent
] ]
}) })
// initialize connection to ollama container // initialize connection to ollama container
export const ollama = new Ollama({ export const ollama = new Ollama({
host: `http://${Keys.ipAddress}:${Keys.portAddress}`, host: `http://${Keys.ipAddress}:${Keys.portAddress}`,
}) })
// Create Queue managed by Events // Create Queue managed by Events
const messageHistory: Queue<UserMessage> = new Queue<UserMessage> const messageHistory: Queue<UserMessage> = new Queue<UserMessage>
// register all events // Create Channel History Queue managed by Events
registerEvents(client, Events, messageHistory, ollama, Keys.defaultModel) const channelMessageHistory: Queue<UserMessage> = new Queue<UserMessage>
// Try to log in the client // register all events
await client.login(Keys.clientToken) registerEvents(client, Events, messageHistory, channelMessageHistory, ollama, Keys.defaultModel)
.catch((error) => {
console.error('[Login Error]', error) // Try to log in the client
process.exit(1) await client.login(Keys.clientToken)
}) .catch((error) => {
console.error('[Login Error]', error)
// queue up bots name process.exit(1)
messageHistory.enqueue({ })
role: 'assistant',
content: `My name is ${client.user?.username}`, // queue up bots name
images: [] messageHistory.enqueue({
role: 'assistant',
content: `My name is ${client.user?.username}`,
images: []
}) })
+3 -1
View File
@@ -9,6 +9,7 @@ import { ClearUserChannelHistory } from './cleanUserChannelHistory.js'
import { PullModel } from './pullModel.js' import { PullModel } from './pullModel.js'
import { SwitchModel } from './switchModel.js' import { SwitchModel } from './switchModel.js'
import { DeleteModel } from './deleteModel.js' import { DeleteModel } from './deleteModel.js'
import { Summary } from './summary.js'
export default [ export default [
ThreadCreate, ThreadCreate,
@@ -20,5 +21,6 @@ export default [
ClearUserChannelHistory, ClearUserChannelHistory,
PullModel, PullModel,
SwitchModel, SwitchModel,
DeleteModel DeleteModel,
Summary
] as SlashCommand[] ] as SlashCommand[]
+50
View File
@@ -0,0 +1,50 @@
import { Client, CommandInteraction, Message, MessageFlags, SendableChannels } from "discord.js";
import { accessChannelContext, normalMessage, SlashCommand, summarizeContextHistory, UserCommand, UserMessage } from "../utils/index.js";
import { ollama } from "../client.js"
export const Summary: SlashCommand = {
name: 'summary',
description: 'provides a summary of the chat history.',
// Generate Summary from additional context
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || !UserCommand.includes(channel.type)) return
// Defer
await interaction.deferReply({ flags: MessageFlags.Ephemeral })
// create summary using context
let channelContext: UserMessage[] = await new Promise((resolve) => {
accessChannelContext(interaction.channelId, (additionalContext) => {
if (additionalContext?.messages)
resolve(additionalContext.messages)
else
resolve([])
})
})
if (channelContext.length === 0) {
interaction.reply({
content: `There are no recent chat messages in this channel.`,
flags: MessageFlags.Ephemeral
})
return
}
// Push summarize prompt
channelContext.push({
role: "user",
content: "Please Summarize everything from the prior messages, provide in bullet point fashion on what people have said prior. For example: \"Someone mentioned/talked about ...\"",
images: []
})
// todo: instead of a default of codellama, we can user default model somehow. Look into later.
const response: string = await summarizeContextHistory(ollama, "codellama", channelContext)
console.log(response)
interaction.editReply({ content: response })
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* @class Logger
* @description A class to handle logging messages
* @method log
*/
export class Logger {
private logPrefix: string = ''
private type: string = 'log'
private constructPrefix(component?: string, method?: string): string {
let prefix = this.type.toUpperCase()
if (component) {
prefix += ` [${component}`
if (method) prefix += `: ${method}`
prefix += ']'
}
return prefix
}
public bind(component?: string, method?: string): CallableFunction {
let tempPrefix = this.constructPrefix(component, method)
if (tempPrefix !== this.logPrefix) this.logPrefix = tempPrefix
switch (this.type) {
case 'warn':
return console.warn.bind(console, this.logPrefix)
case 'error':
return console.error.bind(console, this.logPrefix)
case 'log':
default:
return console.log.bind(console, this.logPrefix)
}
}
public log(type: string, message: unknown, component?: string, method?: string): void {
if (type && type !== this.type) this.type = type
let log = this.bind(component, method)
log(message)
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './queue.js'
export * from './binder.js'
+57 -11
View File
@@ -1,6 +1,6 @@
import { TextChannel } from 'discord.js' import { TextChannel } from 'discord.js'
import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js'
import { import {
event, Events, normalMessage, UserMessage, clean,
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
} from '../utils/index.js' } from '../utils/index.js'
@@ -11,7 +11,7 @@ import {
* *
* @param message the message received from the channel * @param message the message received from the channel
*/ */
export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client, defaultModel }, message) => { export default event(Events.MessageCreate, async ({ log, msgHist, channelHistory, ollama, client, defaultModel }, message) => {
const clientId = client.user!!.id const clientId = client.user!!.id
let cleanedMessage = clean(message.content, clientId) let cleanedMessage = clean(message.content, clientId)
log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`) log(`Message \"${cleanedMessage}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
@@ -19,6 +19,61 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
// Do not respond if bot talks in the chat // Do not respond if bot talks in the chat
if (message.author.username === message.client.user.username) return if (message.author.username === message.client.user.username) return
// Save User Chat even if not for the bot
let channelContextHistory: UserMessage[] = await new Promise((resolve) => {
getChannelInfo(`${message.channelId}-context.json`, (channelInfo) => {
if (channelInfo?.messages)
resolve(channelInfo.messages)
else {
log(`Channel/Thread ${message.channel}-context does not exist. File will be created shortly...`)
resolve([])
}
})
})
if (channelContextHistory.length === 0) {
channelContextHistory = await new Promise((resolve) => {
addToChannelContext(message.channelId,
message.channel as TextChannel
)
getChannelInfo(`${message.channelId}-context.json`, (channelInfo) => {
if (channelInfo?.messages)
resolve(channelInfo.messages)
else {
log(`Channel/Thread ${message.channel}-context does not exist. File will be created shortly...`)
}
})
})
}
// Set Channel History Queue
channelHistory.setQueue(channelContextHistory)
// get message attachment if exists
const attachment = message.attachments.first()
let messageAttachment: string[] = []
if (attachment && attachment.name?.endsWith(".txt"))
cleanedMessage += ' ' + await getTextFileAttachmentData(attachment)
else if (attachment)
messageAttachment = await getAttachmentData(attachment)
while (channelHistory.size() >= channelHistory.capacity) channelHistory.dequeue()
// push user response to channel history
console.log
channelHistory.enqueue({
role: 'user',
content: cleanedMessage,
images: messageAttachment || []
})
// Store in Channel Context
addToChannelContext(message.channelId,
message.channel as TextChannel,
channelHistory.getItems()
)
// Only respond if message mentions the bot // Only respond if message mentions the bot
if (!message.mentions.has(clientId)) return if (!message.mentions.has(clientId)) return
@@ -139,15 +194,6 @@ export default event(Events.MessageCreate, async ({ log, msgHist, ollama, client
if (!userConfig) if (!userConfig)
throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`) throw new Error(`Failed to initialize User Preference for **${message.author.username}**.\n\nIt's likely you do not have a model set. Please use the \`switch-model\` command to do that.`)
// get message attachment if exists
const attachment = message.attachments.first()
let messageAttachment: string[] = []
if (attachment && attachment.name?.endsWith(".txt"))
cleanedMessage += await getTextFileAttachmentData(attachment)
else if (attachment)
messageAttachment = await getAttachmentData(attachment)
const model: string = userConfig.options['switch-model'] const model: string = userConfig.options['switch-model']
// set up new queue // set up new queue
+1 -1
View File
@@ -1 +1 @@
import('./client.js') import('./client.js')
+9 -9
View File
@@ -1,10 +1,10 @@
import { getEnvVar } from './utils/index.js' import { getEnvVar } from './utils/index.js'
export const Keys = { export const Keys = {
clientToken: getEnvVar('CLIENT_TOKEN'), clientToken: getEnvVar('CLIENT_TOKEN'),
ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none ipAddress: getEnvVar('OLLAMA_IP', '127.0.0.1'), // default ollama ip if none
portAddress: getEnvVar('OLLAMA_PORT', '11434'), // default ollama port if none portAddress: getEnvVar('OLLAMA_PORT', '11434'), // default ollama port if none
defaultModel: getEnvVar('MODEL', 'llama3.2') defaultModel: getEnvVar('MODEL', 'llama3.2')
} as const // readonly keys } as const // readonly keys
export default Keys export default Keys
+4 -2
View File
@@ -1,6 +1,6 @@
import type { ClientEvents, Awaitable, Client } from 'discord.js' import type { ClientEvents, Awaitable, Client } from 'discord.js'
import { Ollama } from 'ollama' import { Ollama } from 'ollama'
import { Queue } from '../queues/queue.js' import { Queue } from '../components/index.js'
// Export events through here to reduce amount of imports // Export events through here to reduce amount of imports
export { Events } from 'discord.js' export { Events } from 'discord.js'
@@ -37,6 +37,7 @@ export interface EventProps {
client: Client, client: Client,
log: LogMethod, log: LogMethod,
msgHist: Queue<UserMessage>, msgHist: Queue<UserMessage>,
channelHistory: Queue<UserMessage>,
ollama: Ollama, ollama: Ollama,
defaultModel: String defaultModel: String
} }
@@ -78,6 +79,7 @@ export function registerEvents(
client: Client, client: Client,
events: Event[], events: Event[],
msgHist: Queue<UserMessage>, msgHist: Queue<UserMessage>,
channelHistory: Queue<UserMessage>,
ollama: Ollama, ollama: Ollama,
defaultModel: String defaultModel: String
): void { ): void {
@@ -88,7 +90,7 @@ export function registerEvents(
// Handle Errors, call callback, log errors as needed // Handle Errors, call callback, log errors as needed
try { try {
callback({ client, log, msgHist, ollama, defaultModel }, ...args) callback({ client, log, msgHist, channelHistory, ollama, defaultModel }, ...args)
} catch (error) { } catch (error) {
log('[Uncaught Error]', error) log('[Uncaught Error]', error)
} }
+50 -2
View File
@@ -56,6 +56,54 @@ export async function clearChannelInfo(filename: string, channel: TextChannel, u
return cleanedHistory return cleanedHistory
} }
export async function addToChannelContext(filename: string, channel : TextChannel | ThreadChannel, messages: UserMessage[] = []): Promise<void> {
const fullFileName = `data/${filename}-context.json`
if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error)
console.log(`[Error: addToChannelContext] Incorrect file format`)
else {
const object = JSON.parse(data)
if (object['messages'].length === 0)
object['messages'] = messages as []
else if (object['messages'].length !== 0 && messages.length !== 0)
object['messages'] = messages as []
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
}
})
} else { // channel context does not exist, create it
const object: Configuration = JSON.parse(
`{
\"id\": \"${channel?.id}\",
\"name\": \"${channel?.name}\",
\"messages\": []
}`
)
const directory = path.dirname(fullFileName)
if (!fs.existsSync(directory))
fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
console.log(`[Util: addToChannelContext] Created '${fullFileName}' in working directory`)
}
}
export async function accessChannelContext(filename: string, callback: (config: Channel | undefined) => void): Promise<void> {
const fullFileName = `data/${filename}-context.json`
if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error) {
callback(undefined)
return // something went wrong... stop
}
callback(JSON.parse(data))
})
} else {
callback(undefined) // file not found
}
}
/** /**
* Method to open the channel history * Method to open the channel history
* *
@@ -64,7 +112,7 @@ export async function clearChannelInfo(filename: string, channel: TextChannel, u
* @param user the user's name * @param user the user's name
* @param messages their messages * @param messages their messages
*/ */
export async function openChannelInfo(filename: string, channel: TextChannel | ThreadChannel, user: string, messages: UserMessage[] = []): Promise<void> { export async function openChannelInfo(this: any, filename: string, channel: TextChannel | ThreadChannel, user: string, messages: UserMessage[] = []): Promise<void> {
const fullFileName = `data/${filename}-${user}.json` const fullFileName = `data/${filename}-${user}.json`
if (fs.existsSync(fullFileName)) { if (fs.existsSync(fullFileName)) {
fs.readFile(fullFileName, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
@@ -95,7 +143,7 @@ export async function openChannelInfo(filename: string, channel: TextChannel | T
// only creating it, no need to add anything // only creating it, no need to add anything
fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2)) fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
console.log(`[Util: openChannelInfo] Created '${fullFileName}' in working directory`) console.log(`[Util: ${this.name}] Created '${fullFileName}' in working directory`)
} }
} }
+2 -2
View File
@@ -10,7 +10,7 @@ import path from 'path'
* @param value new value to assign * @param value new value to assign
*/ */
// add type of change (server, user) // add type of change (server, user)
export function openConfig(filename: string, key: string, value: any) { export function openConfig(this: any, filename: string, key: string, value: any) {
const fullFileName = `data/${filename}` const fullFileName = `data/${filename}`
// check if the file exists, if not then make the config file // check if the file exists, if not then make the config file
@@ -41,7 +41,7 @@ export function openConfig(filename: string, key: string, value: any) {
fs.mkdirSync(directory, { recursive: true }) fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2)) fs.writeFileSync(`data/${filename}`, JSON.stringify(object, null, 2))
console.log(`[Util: openConfig] Created '${filename}' in working directory`) console.log(`[Util: ${this.name}] Created '${filename}' in working directory`)
} }
} }
+1 -2
View File
@@ -1,6 +1,5 @@
import { ChatResponse } from "ollama" import { ChatResponse, AbortableAsyncIterator } from "ollama"
import { ChatParams } from "../index.js" import { ChatParams } from "../index.js"
import { AbortableAsyncIterator } from "ollama/src/utils.js"
/** /**
* Method to query the Ollama client for async generation * Method to query the Ollama client for async generation
+48 -9
View File
@@ -1,8 +1,7 @@
import { Message, SendableChannels } from 'discord.js' import { Message, SendableChannels } from 'discord.js'
import { ChatResponse, Ollama } from 'ollama' import { ChatResponse, Ollama, AbortableAsyncIterator } from 'ollama'
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js' import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
import { Queue } from '../queues/queue.js' import { Queue } from '../components/index.js'
import { AbortableAsyncIterator } from 'ollama/src/utils.js'
/** /**
* Method to send replies as normal text on discord like any other user * Method to send replies as normal text on discord like any other user
@@ -11,6 +10,7 @@ import { AbortableAsyncIterator } from 'ollama/src/utils.js'
* @param msgHist message history between user and model * @param msgHist message history between user and model
*/ */
export async function normalMessage( export async function normalMessage(
this: any,
message: Message, message: Message,
ollama: Ollama, ollama: Ollama,
model: string, model: string,
@@ -56,6 +56,10 @@ export async function normalMessage(
response = await blockResponse(params) response = await blockResponse(params)
result = response.message.content result = response.message.content
// check if there is a <think>...</think> sequence from the bot.
if (hasThinking(result))
result = result.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
// check if message length > discord max for normal messages // check if message length > discord max for normal messages
if (result.length > 2000) { if (result.length > 2000) {
sentMessage.edit(result.slice(0, 2000)) sentMessage.edit(result.slice(0, 2000))
@@ -73,15 +77,50 @@ export async function normalMessage(
sentMessage.edit(result) sentMessage.edit(result)
} }
} catch (error: any) { } catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${error.message}`) console.log(`[Util: ${this.name}] Error creating message: ${error.message}`)
if (error.message.includes('fetch failed')) if (error.message.includes('try pulling it first'))
error.message = 'Missing ollama service on machine' sentMessage.edit(`**Response generation failed.**\n\nReason: You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.`)
else if (error.message.includes('try pulling it first')) else
error.message = `You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.` sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
sentMessage.edit(`**Response generation failed.**\n\nReason: ${error.message}`)
} }
}) })
// return the string representation of ollama query response // return the string representation of ollama query response
return result return result
} }
export async function summarizeContextHistory(
ollama: Ollama,
model: string,
msgHist: UserMessage[],
): Promise<string> {
// todo: this stuff to create a response in a simpler way!
try {
const params: ChatParams = {
model: model,
ollama: ollama,
msgHist: msgHist
}
const response: ChatResponse = await blockResponse(params)
let result = response.message.content
if (hasThinking(result))
result = result.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
return result
} catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${error.message}`)
if (error.message.includes('fetch failed'))
error.message = 'Missing ollama service on machine'
else if (error.message.includes('try pulling it first'))
error.message = `You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.`
return `**Response generation failed.**\n\nReason: ${error.message}`
}
}
// Region: Helpers
function hasThinking(message: string): boolean {
return /<think>[\s\S]*?<\/think>/i.test(message)
}
// End Region: Helpers
+77 -77
View File
@@ -1,78 +1,78 @@
// describe marks a test suite // describe marks a test suite
// expect takes a value from an expression // expect takes a value from an expression
// it marks a test case // it marks a test case
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import commands from '../src/commands/index.js' import commands from '../src/commands/index.js'
/** /**
* Mocking client.ts because of the commands * Mocking client.ts because of the commands
*/ */
vi.mock('../src/client.js', () => ({})) vi.mock('../src/client.js', () => ({}))
/** /**
* Commands test suite, tests the commands object * Commands test suite, tests the commands object
* Each command is to be tested elsewhere, this file * Each command is to be tested elsewhere, this file
* is to ensure that the commands object is defined. * is to ensure that the commands object is defined.
* *
* @param name name of the test suite * @param name name of the test suite
* @param fn function holding tests to run * @param fn function holding tests to run
*/ */
describe('Commands Existence', () => { describe('Commands Existence', () => {
// test definition of commands object // test definition of commands object
it('references defined object', () => { it('references defined object', () => {
// toBe compares the value to the expected value // toBe compares the value to the expected value
expect(typeof commands).toBe('object') expect(typeof commands).toBe('object')
}) })
// test specific commands in the object // test specific commands in the object
it('references specific commands', () => { it('references specific commands', () => {
const commandsString = commands.map(e => e.name).join(', ') const commandsString = commands.map(e => e.name).join(', ')
const expectedCommands = ['thread', 'private-thread', 'message-stream', 'toggle-chat', 'shutoff', 'modify-capacity', 'clear-user-channel-history', 'pull-model', 'switch-model', 'delete-model'] const expectedCommands = ['thread', 'private-thread', 'message-stream', 'toggle-chat', 'shutoff', 'modify-capacity', 'clear-user-channel-history', 'pull-model', 'switch-model', 'delete-model', 'summary']
expect(commandsString).toBe(expectedCommands.join(', ')) expect(commandsString).toBe(expectedCommands.join(', '))
}) })
}) })
/** /**
* User Commands Test suite for testing out commands * User Commands Test suite for testing out commands
* that would be run by users when using the application. * that would be run by users when using the application.
*/ */
describe('User Command Tests', () => { describe('User Command Tests', () => {
// test capacity command // test capacity command
it('run modify-capacity command', () => { it('run modify-capacity command', () => {
}) })
it('run clear-user-channel-history command', () => { it('run clear-user-channel-history command', () => {
}) })
it('run message-stream command', () => { it('run message-stream command', () => {
}) })
it('run message-style command', () => { it('run message-style command', () => {
}) })
it('run thread command', () => { it('run thread command', () => {
}) })
it('run private-thread command', () => { it('run private-thread command', () => {
}) })
}) })
/** /**
* Admin Commands Test suite for running administrative * Admin Commands Test suite for running administrative
* commands with the application. * commands with the application.
*/ */
describe('Admin Command Tests', () => { describe('Admin Command Tests', () => {
it('run shutoff command', () => { it('run shutoff command', () => {
}) })
it('run toggle-chat command', () => { it('run toggle-chat command', () => {
}) })
}) })
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { Queue } from '../src/queues/queue.js' import { Queue } from '../src/components/index.js'
/** /**
* Queue test suite, tests the Queue class * Queue test suite, tests the Queue class
+1 -2
View File
@@ -18,9 +18,8 @@
// Decompile .ts to .js into a folder named build // Decompile .ts to .js into a folder named build
"outDir": "build", "outDir": "build",
"rootDir": "src", "rootDir": "src",
"baseUrl": ".",
"paths": { "paths": {
"*": ["node_modules/"] "*": ["./node_modules/"]
} }
}, },
// environment for env vars // environment for env vars
+2 -6
View File
@@ -1,13 +1,9 @@
import { defineConfig, configDefaults } from 'vitest/config' import { defineConfig } from 'vitest/config'
// config for vitest // config for vitest
export default defineConfig({ export default defineConfig({
test: { test: {
globals: true, // <-- reduces test file imports globals: true, // <-- reduces test file imports
reporters: ['verbose'], // <-- verbose output reporters: ['verbose'] // <-- verbose output
coverage: {
exclude: [...configDefaults.exclude, 'build/*', 'tests/*'], // <-- exclude JS build
reporter: ['text-summary'] // <-- report in text-summary
}
} }
}) })