Compare commits

..

3 Commits

Author SHA1 Message Date
Kevin Dang
ae9cac65a9 PR Template Update (#84)
* Update: version increment and reminder on template

* Update: comment on incrementing as necessary
2024-07-11 17:08:34 -07:00
Kevin Dang
61d3dc4312 User Preferences Fix (#83)
* Fix: incorrect user preferences saving
2024-07-10 20:41:23 -07:00
Kevin Dang
35b9ad71cb User vs Server Preferences (#80)
* Update: Server vs User prefs

* Add: User vs Server Prefs

* Update: version increment

* Fix: src and tests added to validation range
2024-07-04 13:54:25 -07:00
14 changed files with 136 additions and 44 deletions

View File

@@ -14,4 +14,6 @@
## After the Pull Request is Opened ## 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). * 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!!!

View File

@@ -6,6 +6,8 @@ on:
- master - master
paths: paths:
- '/' - '/'
- 'src/**'
- 'tests/**'
- '!docs/**' - '!docs/**'
- '!imgs/**' - '!imgs/**'
- '!.github/**' - '!.github/**'

View File

@@ -6,6 +6,8 @@ on:
- master - master
paths: paths:
- '/' - '/'
- 'src/**'
- 'tests/**'
- '!docs/**' - '!docs/**'
- '!imgs/**' - '!imgs/**'
- '!.github/**' - '!.github/**'

View File

@@ -12,7 +12,7 @@
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!
* [ ] User Preferences on Chat * [x] User Preferences on Chat
* [x] Message Persistance on Channels and Threads * [x] Message Persistance on Channels and Threads
* [x] Threads * [x] Threads
* [x] Channels * [x] Channels
@@ -20,10 +20,10 @@ The project aims to:
* [x] Slash Commands Compatible * [x] Slash Commands 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
* [ ] User vs. Server Preferences * [x] User vs. Server Preferences
* [ ] Redis Caching * [ ] Redis Caching
* [x] Administrator Role Compatible * [x] Administrator Role Compatible
* [ ] Multi-User Chat Generation (Multiple users chatting at the same time) * [x] Multi-User Chat Generation (Multiple users chatting at the same time) - This was built into from Ollama `v0.2.1+`
* [ ] Automatic and Manual model pulling through the Discord client * [ ] Automatic and Manual model pulling through the Discord client
* [ ] Allow others to create their own models personalized for their own servers! * [ ] Allow others to create their own models personalized for their own servers!
* [ ] Documentation on creating your own LLM * [ ] Documentation on creating your own LLM

View File

@@ -8,7 +8,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: discord/bot:0.5.2 image: discord/bot:0.5.5
environment: environment:
CLIENT_TOKEN: ${CLIENT_TOKEN} CLIENT_TOKEN: ${CLIENT_TOKEN}
GUILD_ID: ${GUILD_ID} GUILD_ID: ${GUILD_ID}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.5.2", "version": "0.5.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.5.2", "version": "0.5.5",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.15.3", "discord.js": "^14.15.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "discord-ollama", "name": "discord-ollama",
"version": "0.5.2", "version": "0.5.5",
"description": "Ollama Integration into discord", "description": "Ollama Integration into discord",
"main": "build/index.js", "main": "build/index.js",
"exports": "./build/index.js", "exports": "./build/index.js",

View File

@@ -23,7 +23,7 @@ export const Capacity: SlashCommand = {
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
// set state of bot chat features // set state of bot chat features
openConfig('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({ interaction.reply({
content: `Message History Capacity has been set to \`${interaction.options.get('context-capacity')?.value}\``, content: `Message History Capacity has been set to \`${interaction.options.get('context-capacity')?.value}\``,

View File

@@ -24,7 +24,7 @@ export const ChannelToggle: SlashCommand = {
// set state of bot channel preferences // set state of bot channel preferences
openConfig('config.json', interaction.commandName, interaction.options.get('toggle-channel')?.value) openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('toggle-channel')?.value)
interaction.reply({ interaction.reply({
content: `Channel Preferences have for Regular Channels set to \`${interaction.options.get('toggle-channel')?.value}\``, content: `Channel Preferences have for Regular Channels set to \`${interaction.options.get('toggle-channel')?.value}\``,

View File

@@ -32,7 +32,7 @@ export const Disable: SlashCommand = {
} }
// set state of bot chat features // set state of bot chat features
openConfig('config.json', interaction.commandName, interaction.options.get('enabled')?.value) openConfig(`${interaction.guildId}-config.json`, interaction.commandName, interaction.options.get('enabled')?.value)
interaction.reply({ interaction.reply({
content: `Chat features has been \`${interaction.options.get('enabled')?.value ? "enabled" : "disabled" }\``, content: `Chat features has been \`${interaction.options.get('enabled')?.value ? "enabled" : "disabled" }\``,

View File

@@ -23,7 +23,7 @@ export const MessageStream: SlashCommand = {
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
// save value to json and write to it // save value to json and write to it
openConfig('config.json', interaction.commandName, interaction.options.get('stream')?.value) openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('stream')?.value)
interaction.reply({ interaction.reply({
content: `Message streaming preferences set to: \`${interaction.options.get('stream')?.value}\``, content: `Message streaming preferences set to: \`${interaction.options.get('stream')?.value}\``,

View File

@@ -23,7 +23,7 @@ export const MessageStyle: SlashCommand = {
if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return if (!channel || channel.type !== (ChannelType.PublicThread && ChannelType.GuildText)) return
// set the message style // set the message style
openConfig('config.json', interaction.commandName, interaction.options.get('embed')?.value) openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('embed')?.value)
interaction.reply({ interaction.reply({
content: `Message style preferences for embed set to: \`${interaction.options.get('embed')?.value}\``, content: `Message style preferences for embed set to: \`${interaction.options.get('embed')?.value}\``,

View File

@@ -1,30 +1,38 @@
import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.js' import { embedMessage, event, Events, normalMessage, UserMessage } from '../utils/index.js'
import { Configuration, getChannelInfo, getConfig, getThread, openChannelInfo, openConfig, openThreadInfo } from '../utils/jsonHandler.js' import { getChannelInfo, getServerConfig, getThread, getUserConfig, openChannelInfo, openConfig, openThreadInfo, ServerConfig, UserConfig } from '../utils/jsonHandler.js'
import { clean } from '../utils/mentionClean.js' import { clean } from '../utils/mentionClean.js'
import { TextChannel, ThreadChannel } from 'discord.js' import { TextChannel, ThreadChannel } from 'discord.js'
/** /**
* Max Message length for free users is 2000 characters (bot or not). * Max Message length for free users is 2000 characters (bot or not).
* Bot supports infinite lengths for normal messages.
*
* @param message the message received from the channel * @param message the message received from the channel
*/ */
export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama, client }, message) => { 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}.`) log(`Message \"${clean(message.content)}\" from ${message.author.tag} in channel/thread ${message.channelId}.`)
// Do not respond if bot talks in the chat // Do not respond if bot talks in the chat
if (message.author.tag === message.client.user.tag) return if (message.author.username === message.client.user.username) return
// Only respond if message mentions the bot // Only respond if message mentions the bot
if (!message.mentions.has(tokens.clientUid)) return if (!message.mentions.has(tokens.clientUid)) return
// default stream to false
let shouldStream = false let shouldStream = false
// Try to query and send embed
try { try {
const config: Configuration = await new Promise((resolve, reject) => { // Retrieve Server/Guild Preferences
getConfig('config.json', (config) => { const serverConfig: ServerConfig = await new Promise((resolve, reject) => {
getServerConfig(`${message.guildId}-config.json`, (config) => {
// check if config.json exists // check if config.json exists
if (config === undefined) { if (config === undefined) {
reject(new Error('No Configuration is set up.\n\nCreating \`config.json\` with \`message-style\` set as \`false\` for regular messages.\nPlease try chatting again.')) // 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 return
} }
@@ -38,10 +46,23 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
if (config.options['channel-toggle']) { if (config.options['channel-toggle']) {
openChannelInfo(message.channelId, openChannelInfo(message.channelId,
message.channel as TextChannel, message.channel as TextChannel,
message.author.tag message.author.username
) )
} }
resolve(config)
})
})
// Retrieve User Preferences
const userConfig: UserConfig = await new Promise((resolve, reject) => {
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.'))
return
}
// check if there is a set capacity in config // check if there is a set capacity in config
if (typeof config.options['modify-capacity'] !== 'number') if (typeof config.options['modify-capacity'] !== 'number')
log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`) log(`Capacity is undefined, using default capacity of ${msgHist.capacity}.`)
@@ -51,23 +72,25 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
log(`New Capacity found. Setting Context Capacity to ${config.options['modify-capacity']}.`) log(`New Capacity found. Setting Context Capacity to ${config.options['modify-capacity']}.`)
msgHist.capacity = config.options['modify-capacity'] msgHist.capacity = config.options['modify-capacity']
} }
// set stream state // set stream state
shouldStream = config.options['message-stream'] as boolean || false shouldStream = config.options['message-stream'] as boolean || false
resolve(config) resolve(config)
}) })
}) })
// need new check for "open/active" threads/channels here! // need new check for "open/active" threads/channels here!
const chatMessages: UserMessage[] = await new Promise((resolve) => { const chatMessages: UserMessage[] = await new Promise((resolve) => {
// set new queue to modify // set new queue to modify
if (config.options['channel-toggle']) { if (serverConfig.options['channel-toggle']) {
getChannelInfo(`${message.channelId}-${message.author.tag}.json`, (channelInfo) => { getChannelInfo(`${message.channelId}-${message.author.username}.json`, (channelInfo) => {
if (channelInfo?.messages) if (channelInfo?.messages)
resolve(channelInfo.messages) resolve(channelInfo.messages)
else else
log(`Channel ${message.channel}-${message.author.tag} does not exist.`) log(`Channel ${message.channel}-${message.author.username} does not exist.`)
}) })
} else { } else {
getThread(`${message.channelId}.json`, (threadInfo) => { getThread(`${message.channelId}.json`, (threadInfo) => {
@@ -95,7 +118,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
}) })
// undefined or false, use normal, otherwise use embed // undefined or false, use normal, otherwise use embed
if (config.options['message-style']) if (userConfig.options['message-style'])
response = await embedMessage(message, ollama, tokens, msgHist, shouldStream) response = await embedMessage(message, ollama, tokens, msgHist, shouldStream)
else else
response = await normalMessage(message, ollama, tokens, msgHist, shouldStream) response = await normalMessage(message, ollama, tokens, msgHist, shouldStream)
@@ -113,7 +136,7 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
}) })
// only update the json on success // only update the json on success
if (config.options['channel-toggle']) { if (serverConfig.options['channel-toggle']) {
openChannelInfo(message.channelId, openChannelInfo(message.channelId,
message.channel as TextChannel, message.channel as TextChannel,
message.author.tag, message.author.tag,
@@ -127,7 +150,6 @@ export default event(Events.MessageCreate, async ({ log, msgHist, tokens, ollama
} }
} catch (error: any) { } catch (error: any) {
msgHist.pop() // remove message because of failure msgHist.pop() // remove message because of failure
openConfig('config.json', 'message-style', false)
message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`) message.reply(`**Error Occurred:**\n\n**Reason:** *${error.message}*`)
} }
}) })

View File

@@ -3,15 +3,39 @@ import { UserMessage } from './events.js'
import fs from 'fs' import fs from 'fs'
import path from 'path' 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 { export interface Configuration {
readonly name: string readonly name: string
options: { options: UserConfiguration | ServerConfiguration
'message-stream'?: boolean, }
'message-style'?: boolean,
'toggle-chat'?: boolean, /**
'modify-capacity'?: number, * User config to use outside of this file
'channel-toggle'?: boolean */
} export interface UserConfig {
readonly name: string
options: UserConfiguration
}
export interface ServerConfig {
readonly name: string
options: ServerConfiguration
} }
export interface Thread { export interface Thread {
@@ -27,6 +51,14 @@ export interface Channel {
messages: UserMessage[] 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 * Method to open a file in the working directory and modify/create it
* *
@@ -34,27 +66,34 @@ export interface Channel {
* @param key key value to access * @param key key value to access
* @param value new value to assign * @param value new value to assign
*/ */
// add type of change (server, user)
export function openConfig(filename: string, key: string, value: any) { 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 // check if the file exists, if not then make the config file
if (fs.existsSync(filename)) { if (fs.existsSync(fullFileName)) {
fs.readFile(filename, 'utf8', (error, data) => { fs.readFile(fullFileName, 'utf8', (error, data) => {
if (error) if (error)
console.log(`[Error: openConfig] Incorrect file format`) console.log(`[Error: openConfig] Incorrect file format`)
else { else {
const object = JSON.parse(data) const object = JSON.parse(data)
object['options'][key] = value object['options'][key] = value
fs.writeFileSync(filename, JSON.stringify(object, null, 2)) fs.writeFileSync(fullFileName, JSON.stringify(object, null, 2))
} }
}) })
} else { // work on dynamic file creation } else { // work on dynamic file creation
const object: Configuration = JSON.parse('{ \"name\": \"Discord Ollama Confirgurations\" }') 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 // set standard information for config file and options
object['options'] = { object['options'] = {
[key]: value [key]: value
} }
fs.writeFileSync(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: openConfig] Created '${filename}' in working directory`)
} }
} }
@@ -65,10 +104,35 @@ export function openConfig(filename: string, key: string, value: any) {
* @param filename name of the configuration file to get * @param filename name of the configuration file to get
* @param callback function to allow a promise from getting the config * @param callback function to allow a promise from getting the config
*/ */
export async function getConfig(filename: string, callback: (config: Configuration | undefined) => void): Promise<void> { 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 // attempt to read the file and get the configuration
if (fs.existsSync(filename)) { if (fs.existsSync(fullFileName)) {
fs.readFile(filename, 'utf8', (error, data) => { 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) { if (error) {
callback(undefined) callback(undefined)
return // something went wrong... stop return // something went wrong... stop