Compare commits

...

4 Commits

Author SHA1 Message Date
Kevin Dang
33152b33f3 Pull/Switch Model Commands Fix (#137)
* Update: Channel checker and channel name gone

* Add: note of where problem can be

* Update: Check if model already exists for Pull Command

* Add: User/Admin Command Constants

* Update: version increment
2024-11-08 20:09:01 -08:00
Jonathan Smoley
1ccd1a012e Roll NPM Dependencies Forward (#136)
* update channel as sendable

* Update: Casting SendableChannels Once

* Remove: another semicolon

* Update: version increment

---------

Co-authored-by: kevinthedang <kevinthedang_1@outlook.com>
2024-11-08 10:13:16 -08:00
Jonathan Smoley
68a5e097fe Feature Set Documentation (#130)
* added client events documentation

* wording updated
2024-10-19 16:46:51 -07:00
Kevin Dang
624ff2e5c8 Add: Slash Commands Guide (#128) 2024-10-16 10:07:40 -07:00
21 changed files with 845 additions and 444 deletions

View File

@@ -11,6 +11,7 @@
* features: `'feature/**'`
* releases: `'releases/**'`
* bugs: `'bug/**'`
* docs: `'docs/**'`
## Run the Bot
* Refer to all sections below before running the bot.

View File

@@ -31,6 +31,11 @@ The project aims to:
* [ ] Documentation on creating your own LLM
* [ ] Documentation on web scrapping and cleaning
## Documentation
These are guides to the feature set included and the events triggered in this app.
* [User Slash Commands](./docs/commands-guide.md)
* [Client Events](./docs/events-guide.md)
## Environment Setup
* Clone this repo using `git clone https://github.com/kevinthedang/discord-ollama.git` or just use [GitHub Desktop](https://desktop.github.com/) to clone the repo.
* You will need a `.env` file in the root of the project directory with the bot's token. There is a `.env.sample` is provided for you as a reference for what environment variables.
@@ -41,6 +46,7 @@ The project aims to:
* [Docker Setup for Servers and Local Machines](./docs/setup-docker.md)
* Nvidia is recommended for now, but support for other GPUs should be development.
* Local use is not recommended.
## Resources
* [NodeJS](https://nodejs.org/en)
* This project runs on `lts\hydrogen`.

View File

@@ -7,7 +7,7 @@ services:
build: ./ # find docker file in designated path
container_name: discord
restart: always # rebuild container always
image: kevinthedang/discord-ollama:0.7.0
image: kevinthedang/discord-ollama:0.7.2
environment:
CLIENT_TOKEN: ${CLIENT_TOKEN}
OLLAMA_IP: ${OLLAMA_IP}

105
docs/commands-guide.md Normal file
View File

@@ -0,0 +1,105 @@
## Commands Guide
This is a guide to all of the slash commands for the app.
* Action Commands are commands that do not affect a user's `preference file`.
* Guild Commands can also be considered action commands.
> [!NOTE]
> Administrator commands are only usable by actual administrators on the Discord server.
### Guild Commands (Administrator)
1. Disable (or Toggle Chat)
This command will `enable` or `disable` whether or not the app will respond to users.
```
/toggle-chat enabled true
```
2. Shutoff
This command will shutoff the app so no users can converse with it.
The app must be manually restarted upon being shutoff.
Below shuts off the app by putting `true` in the `are-your-sure` field.
```
/shutoff are-you-sure true
```
### Action Commands
1. Clear Channel (Message) History
This command will clear the history of the current channel for the user that calls it.
Running the command in any channel will clear the message history.
```
/clear-user-channel-history
```
2. Pull Model
This command will pull a model that exists on the [Ollama Model Library](https://ollama.com/library). If it does not exist there, it will throw a hissy fit.
Below trys to pull the `codellama` model.
```
/pull-model model-to-pull codellama
```
3. Thread Create
This command creates a public thread to talk with the app instead of using a `GuildText` channel.
```
/thread
```
4. (Private) Thread Create
This command creates a private thread to talk with the bot privately.
Invite others to the channel and they will be able to talk to the app as well.
```
/private-thread
```
### User Preference Commands
1. Capacity
This command changes how much context it will keep in conversations with the app.
This is applied for all of existing chats when interacting with the app.
Below sets the message history capacity to at most 5 messages at once.
```
/modify-capacity context-capacity 5
```
2. Message Stream
This command will toggle whether or not the app will "stream" a response.
(think of how ChatGPT and other interfaces do this)
Below sets the `stream` to true to make the app respond in increments.
```
/message-stream stream true
```
> [!NOTE]
> This is a very slow progress on Discord because "spamming" changes within 5 seconds is not allowed.
3. Message Style
This command allows a user to select whether to embed the app's response.
```
/message-style embed true
```
This allows the app to respond as a user would normally respond.
```
/message-style embed false
```
4. Switch Model
This command will switch the user-preferred model so long as it exists in within the local ollama service or from the [Ollama Model Library](https://ollama.com/library).
If it cannot be found locally, it will attempt to find it in the model library.
Below we are trying to switch to a specific model size.
```
/switch-model model-to-use llama3.2:1.3b
```

27
docs/events-guide.md Normal file
View File

@@ -0,0 +1,27 @@
## Events Guide
This is a guide to all of the client events for the app.
> [!NOTE] Each of these is logged to the console for a developer to track.
1. ClientReady
This event signifies that the Discord app is online.
Here the app's activity is set and its commands are registered.
2. InteractionCreate
This event signifies that a user interacted from Discord in some way.
Here commands are selected from a knowledge bank and executed if found.
> [!NOTE] Possible interactions include commands, buttons, menus, etc.
3. MessageCreate
This event signifies that a message was sent.
Here user questions and comments for the LLM are processed.
1. check message is from a user and mentions the app
2. check for interaction preferences
3. add the message to a queue
4. check the response for success
5. send a response back to the user.
4. ThreadDelete
This event signifies that a Discord Thread was deleted.
Here any preferences set for interaction within the thread are cleared away.

1009
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "discord-ollama",
"version": "0.7.0",
"version": "0.7.2",
"description": "Ollama Integration into discord",
"main": "build/index.js",
"exports": "./build/index.js",
@@ -26,17 +26,17 @@
"author": "Kevin Dang",
"license": "ISC",
"dependencies": {
"discord.js": "^14.15.3",
"discord.js": "^14.16.3",
"dotenv": "^16.4.5",
"ollama": "^0.5.9"
},
"devDependencies": {
"@types/node": "^22.7.5",
"@vitest/coverage-v8": "^2.1.2",
"@types/node": "^22.9.0",
"@vitest/coverage-v8": "^2.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.19.1",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"vitest": "^2.1.2"
"vitest": "^2.1.4"
},
"type": "module",
"engines": {

View File

@@ -1,5 +1,5 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { openConfig, SlashCommand } from '../utils/index.js'
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
export const Capacity: SlashCommand = {
name: 'modify-capacity',
@@ -19,7 +19,7 @@ export const Capacity: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
if (!channel || !UserCommand.includes(channel.type)) return
// set state of bot chat features
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('context-capacity')?.value)

View File

@@ -1,5 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel } from 'discord.js'
import { clearChannelInfo, SlashCommand } from '../utils/index.js'
import { Channel, Client, CommandInteraction, TextChannel } from 'discord.js'
import { clearChannelInfo, SlashCommand, UserCommand } from '../utils/index.js'
export const ClearUserChannelHistory: SlashCommand = {
name: 'clear-user-channel-history',
@@ -8,10 +8,10 @@ export const ClearUserChannelHistory: SlashCommand = {
// Clear channel history for intended user
run: async (client: Client, interaction: CommandInteraction) => {
// fetch current channel
const channel = await client.channels.fetch(interaction.channelId)
const channel: Channel | null = await client.channels.fetch(interaction.channelId)
// if not an existing channel or a GuildText, fail command
if (!channel || channel.type !== ChannelType.GuildText) return
if (!channel || !UserCommand.includes(channel.type)) return
// clear channel info for user
const successfulWipe = await clearChannelInfo(interaction.channelId,
@@ -21,12 +21,12 @@ export const ClearUserChannelHistory: SlashCommand = {
// check result of clearing history
if (successfulWipe)
interaction.reply({
content: `Channel history in **${channel.name}** cleared for **${interaction.user.username}**.`,
content: `Channel history in **this channel** successfully cleared for **${interaction.user.username}**.`,
ephemeral: true
})
else
interaction.reply({
content: `Channel history could not be found for **${interaction.user.username}** in **${channel.name}**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`,
content: `Channel history could not be found for **${interaction.user.username}** in **this channel**.\n\nPlease chat with **${client.user?.username}** to start a chat history.`,
ephemeral: true
})
}

View File

@@ -1,5 +1,5 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { openConfig, SlashCommand } from '../utils/index.js'
import { AdminCommand, openConfig, SlashCommand } from '../utils/index.js'
export const Disable: SlashCommand = {
name: 'toggle-chat',
@@ -19,7 +19,7 @@ export const Disable: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
if (!channel || !AdminCommand.includes(channel.type)) return
// check if runner is an admin
if (!interaction.memberPermissions?.has('Administrator')) {

View File

@@ -1,5 +1,5 @@
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from 'discord.js'
import { openConfig, SlashCommand } from '../utils/index.js'
import { ApplicationCommandOptionType, Client, CommandInteraction } from 'discord.js'
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
export const MessageStream: SlashCommand = {
name: 'message-stream',
@@ -19,7 +19,7 @@ export const MessageStream: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// verify channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
if (!channel || !UserCommand.includes(channel.type)) return
// save value to json and write to it
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('stream')?.value)

View File

@@ -1,5 +1,5 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { openConfig, SlashCommand } from '../utils/index.js'
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { openConfig, SlashCommand, UserCommand } from '../utils/index.js'
export const MessageStyle: SlashCommand = {
name: 'message-style',
@@ -19,7 +19,7 @@ export const MessageStyle: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
if (!channel || !UserCommand.includes(channel.type)) return
// set the message style
openConfig(`${interaction.user.username}-config.json`, interaction.commandName, interaction.options.get('embed')?.value)

View File

@@ -1,6 +1,8 @@
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from "discord.js";
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js";
import { SlashCommand } from "../utils/commands.js";
import { ollama } from "../client.js";
import { ModelResponse } from "ollama";
import { UserCommand } from "../utils/index.js";
export const PullModel: SlashCommand = {
name: 'pull-model',
@@ -24,13 +26,16 @@ export const PullModel: SlashCommand = {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
if (!channel || !UserCommand.includes(channel.type)) return
// check if model was already pulled
const modelExists: boolean = await ollama.list()
.then(response => response.models.some((model: ModelResponse) => model.name.startsWith(modelInput)))
try {
// call ollama to pull desired model
await ollama.pull({
model: modelInput
})
if (!modelExists)
await ollama.pull({ model: modelInput })
} catch (error) {
// could not resolve pull or model unfound
interaction.editReply({
@@ -39,9 +44,14 @@ export const PullModel: SlashCommand = {
return
}
// successful pull
interaction.editReply({
content: `Successfully added **${modelInput}** into your local model library.`
})
// successful interaction
if (modelExists)
interaction.editReply({
content: `**${modelInput}** is already in your local model library.`
})
else
interaction.editReply({
content: `Successfully added **${modelInput}** into your local model library.`
})
}
}

View File

@@ -1,5 +1,6 @@
import { ChannelType, Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { Client, CommandInteraction, ApplicationCommandOptionType } from 'discord.js'
import { SlashCommand } from '../utils/commands.js'
import { AdminCommand } from '../utils/index.js'
export const Shutoff: SlashCommand = {
name: 'shutoff',
@@ -19,7 +20,7 @@ export const Shutoff: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
if (!channel || !AdminCommand.includes(channel.type)) return
// log this, this will probably be improtant for logging who did this
console.log(`User -> ${interaction.user.tag} attempting to shutdown ${client.user!!.tag}`)

View File

@@ -1,8 +1,8 @@
import { ApplicationCommandOptionType, ChannelType, Client, CommandInteraction } from "discord.js";
import { ApplicationCommandOptionType, Client, CommandInteraction } from "discord.js";
import { SlashCommand } from "../utils/commands.js";
import { ollama } from "../client.js";
import { ModelResponse } from "ollama";
import { openConfig } from "../utils/index.js";
import { openConfig, UserCommand } from "../utils/index.js";
export const SwitchModel: SlashCommand = {
name: 'switch-model',
@@ -26,7 +26,7 @@ export const SwitchModel: SlashCommand = {
// fetch channel and message
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== (ChannelType.PrivateThread && ChannelType.PublicThread && ChannelType.GuildText)) return
if (!channel || !UserCommand.includes(channel.type)) return
try {
// Phase 1: Set the model
@@ -46,6 +46,7 @@ export const SwitchModel: SlashCommand = {
}
}
})
// todo: problem can be here if async messes up
if (switchSuccess) return
// Phase 2: Try to get it regardless

View File

@@ -1,5 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
import { openChannelInfo, SlashCommand } from '../utils/index.js'
import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js'
export const ThreadCreate: SlashCommand = {
name: 'thread',
@@ -9,7 +9,7 @@ export const ThreadCreate: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// fetch the channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
if (!channel || !AdminCommand.includes(channel.type)) return
const thread = await (channel as TextChannel).threads.create({
name: `${client.user?.username}-support-${Date.now()}`,

View File

@@ -1,5 +1,5 @@
import { ChannelType, Client, CommandInteraction, TextChannel, ThreadChannel } from 'discord.js'
import { openChannelInfo, SlashCommand } from '../utils/index.js'
import { AdminCommand, openChannelInfo, SlashCommand } from '../utils/index.js'
export const PrivateThreadCreate: SlashCommand = {
name: 'private-thread',
@@ -9,7 +9,7 @@ export const PrivateThreadCreate: SlashCommand = {
run: async (client: Client, interaction: CommandInteraction) => {
// fetch the channel
const channel = await client.channels.fetch(interaction.channelId)
if (!channel || channel.type !== ChannelType.GuildText) return
if (!channel || !AdminCommand.includes(channel.type)) return
const thread = await (channel as TextChannel).threads.create({
name: `${client.user?.username}-private-support-${Date.now()}`,

View File

@@ -1,3 +1,4 @@
import { ChannelType } from 'discord.js'
import { UserMessage } from './index.js'
export interface UserConfiguration {
@@ -42,6 +43,21 @@ export interface Channel {
messages: UserMessage[]
}
/**
* The following 2 types is allow for better readability in commands
* Admin Command -> Don't run in Threads
* User Command -> Used anywhere
*/
export const AdminCommand = [
ChannelType.GuildText
]
export const UserCommand = [
ChannelType.GuildText,
ChannelType.PublicThread,
ChannelType.PrivateThread
]
/**
* Check if the configuration we are editing/taking from is a Server Config
* @param key name of command we ran

View File

@@ -53,7 +53,6 @@ export async function clearChannelInfo(filename: string, channel: TextChannel, u
}
})
})
console.log(cleanedHistory)
return cleanedHistory
}

View File

@@ -1,4 +1,4 @@
import { EmbedBuilder, Message } from 'discord.js'
import { EmbedBuilder, Message, SendableChannels } from 'discord.js'
import { ChatResponse, Ollama } from 'ollama'
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
import { Queue } from '../queues/queue.js'
@@ -28,7 +28,8 @@ export async function embedMessage(
.setColor('#00FF00')
// send the message
const sentMessage = await message.channel.send({ embeds: [botMessage] })
const channel = message.channel as SendableChannels
const sentMessage = await channel.send({ embeds: [botMessage] })
// create params
const params: ChatParams = {
@@ -48,12 +49,12 @@ export async function embedMessage(
// exceeds handled length
if (result.length > 5000) {
const errorEmbed = new EmbedBuilder()
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(`Response length ${result.length} has exceeded Discord maximum.\n\nLong Stream messages not supported.`)
.setColor('#00FF00')
.setTitle(`Responding to ${message.author.tag}`)
.setDescription(`Response length ${result.length} has exceeded Discord maximum.\n\nLong Stream messages not supported.`)
.setColor('#00FF00')
// send error
message.channel.send({ embeds: [errorEmbed] })
channel.send({ embeds: [errorEmbed] })
break // cancel loop and stop
}
@@ -90,7 +91,7 @@ export async function embedMessage(
.setDescription(result.slice(0, 5000) || 'No Content to Provide...')
.setColor('#00FF00')
message.channel.send({ embeds: [whileEmbed] })
channel.send({ embeds: [whileEmbed] })
result = result.slice(5000)
}
@@ -100,7 +101,7 @@ export async function embedMessage(
.setColor('#00FF00')
// rest of the response
message.channel.send({ embeds: [lastEmbed] })
channel.send({ embeds: [lastEmbed] })
} else {
// only need to create 1 embed, handles 6000 characters
const newEmbed = new EmbedBuilder()

View File

@@ -1,4 +1,4 @@
import { Message } from 'discord.js'
import { Message, SendableChannels } from 'discord.js'
import { ChatResponse, Ollama } from 'ollama'
import { ChatParams, UserMessage, streamResponse, blockResponse } from './index.js'
import { Queue } from '../queues/queue.js'
@@ -20,8 +20,9 @@ export async function normalMessage(
// bot's respnse
let response: ChatResponse | AbortableAsyncIterator<ChatResponse>
let result: string = ''
const channel = message.channel as SendableChannels
await message.channel.send('Generating Response . . .').then(async sentMessage => {
await channel.send('Generating Response . . .').then(async sentMessage => {
try {
const params: ChatParams = {
model: model,
@@ -39,7 +40,7 @@ export async function normalMessage(
result = portion.message.content
// new message block, wait for it to send and assign new block to respond.
await message.channel.send("Creating new stream block...").then(sentMessage => { messageBlock = sentMessage })
await channel.send("Creating new stream block...").then(sentMessage => { messageBlock = sentMessage })
} else {
result += portion.message.content
@@ -61,12 +62,12 @@ export async function normalMessage(
// handle for rest of message that is >2000
while (result.length > 2000) {
message.channel.send(result.slice(0, 2000))
channel.send(result.slice(0, 2000))
result = result.slice(2000)
}
// last part of message
message.channel.send(result)
channel.send(result)
} else // edit the 'generic' response to new message since <2000
sentMessage.edit(result)
}