Compare commits

..
12 Commits
21 changed files with 1002 additions and 693 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
+27 -21
View File
@@ -1,28 +1,32 @@
name: Coverage name: Coverage
run-name: Code Coverage run-name: Code Coverage
on: on:
push: pull_request:
branches: branches:
- master - master
types: [opened, synchronize, reopened, closed]
permissions:
contents: write
jobs: jobs:
Discord-Node-Coverage: Discord-Node-Coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: coverage
timeout-minutes: 2 timeout-minutes: 2
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node Environment lts/jod - name: Set up Node Environment
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: lts/jod node-version: lts/jod
cache: "npm" cache: npm
- name: Install Project Dependencies - name: Install Project Dependencies
run: | run: npm install
npm install
- name: Create Environment Variables - name: Create Environment Variables
run: | run: |
@@ -32,19 +36,21 @@ jobs:
echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env echo OLLAMA_PORT = ${{ secrets.OLLAMA_PORT }} >> .env
echo MODEL = ${{ secrets.MODEL }} >> .env echo MODEL = ${{ secrets.MODEL }} >> .env
- name: Collect Code Coverage - name: Run tests with coverage
run: | run: npm run coverage
LINE_PCT=$(npm run coverage | tail -2 | head -1 | awk '{print $3}')
echo "COVERAGE=$LINE_PCT" >> $GITHUB_ENV - name: Generate coverage badge
uses: wjervis7/vitest-badge-action@v1.0.0
- name: Upload Code Coverage if: success() || failure()
uses: schneegans/dynamic-badges-action@v1.7.0
with: with:
auth: ${{ secrets.GIST_SECRET }} result-type: statements
gistID: ${{ vars.GIST_ID }} badge-text: 'Coverage (Statements)'
filename: coverage.json badge-path: ./imgs/coverage.svg
label: Coverage upload-badge: false
message: ${{ env.COVERAGE }}
valColorRange: ${{ env.COVERAGE }} - name: Commit badge with merge
maxColorRange: 100 if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true
minColorRange: 0 uses: stefanzweifel/git-auto-commit-action@v7
with:
file_pattern: ./imgs/coverage.svg
commit_message: 'update with coverage badge'
+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
+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
+21 -12
View File
@@ -4,28 +4,37 @@
<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="Coverage" src="https://github.com/kevinthedang/discord-ollama/blob/main/imgs/coverage.svg?raw=true" /></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}
+484 -384
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"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",
+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 './queues/queue.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 })
}
}
+57 -11
View File
@@ -1,5 +1,5 @@
import { TextChannel } from 'discord.js' import { TextChannel } from 'discord.js'
import { event, Events, normalMessage, UserMessage, clean } from '../utils/index.js' import { event, Events, normalMessage, UserMessage, clean, addToChannelContext } from '../utils/index.js'
import { import {
getChannelInfo, getServerConfig, getUserConfig, openChannelInfo, getChannelInfo, getServerConfig, getUserConfig, openChannelInfo,
openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData openConfig, UserConfig, getAttachmentData, getTextFileAttachmentData
@@ -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
+3 -1
View File
@@ -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)
} }
+48
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
* *
+40
View File
@@ -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))
@@ -85,3 +89,39 @@ export async function normalMessage(
// return the string representation of ollama query response // return the string representation of ollama query response
return result return result
} }
export async function summarizeContextHistory(
ollama: Ollama,
model: string,
msgHist: UserMessage[],
): Promise<string> {
// todo: this stuff to create a response in a simpler way!
try {
const params: ChatParams = {
model: model,
ollama: ollama,
msgHist: msgHist
}
const response: ChatResponse = await blockResponse(params)
let result = response.message.content
if (hasThinking(result))
result = result.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
return result
} catch (error: any) {
console.log(`[Util: messageNormal] Error creating message: ${error.message}`)
if (error.message.includes('fetch failed'))
error.message = 'Missing ollama service on machine'
else if (error.message.includes('try pulling it first'))
error.message = `You do not have the ${model} downloaded. Ask an admin to pull it using the \`pull-model\` command.`
return `**Response generation failed.**\n\nReason: ${error.message}`
}
}
// Region: Helpers
function hasThinking(message: string): boolean {
return /<think>[\s\S]*?<\/think>/i.test(message)
}
// End Region: Helpers
+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', () => {
}) })
}) })
+3 -3
View File
@@ -1,4 +1,4 @@
import { defineConfig, configDefaults } from 'vitest/config' import { defineConfig } from 'vitest/config'
// config for vitest // config for vitest
export default defineConfig({ export default defineConfig({
@@ -6,8 +6,8 @@ export default defineConfig({
globals: true, // <-- reduces test file imports globals: true, // <-- reduces test file imports
reporters: ['verbose'], // <-- verbose output reporters: ['verbose'], // <-- verbose output
coverage: { coverage: {
exclude: [...configDefaults.exclude, 'build/*', 'tests/*'], // <-- exclude JS build include: ['src/**/*.ts'],
reporter: ['text-summary'] // <-- report in text-summary reporter: 'json-summary'
} }
} }
}) })