feat: Add AI module
ollama based ai module for more bot entertaiment
This commit is contained in:
10 files changed
+394
-2
No files matched your search
@@ -47,12 +47,19 @@ const dev = {
|
||||
guild: testGuild
|
||||
};
|
||||
|
||||
/** AI module secrets. */
|
||||
const llama = {
|
||||
apiUrl: process.env.LLAMA_API_URL,
|
||||
model: process.env.LLAMA_MODEL
|
||||
};
|
||||
|
||||
/** Exported bundled configuration object. */
|
||||
const config = {
|
||||
application,
|
||||
colors,
|
||||
path,
|
||||
dev
|
||||
dev,
|
||||
llama
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* /ask
|
||||
*
|
||||
* Sends a prompt to the locally deployed Llama LLM and returns the response.
|
||||
* Maintains conversation history per user and guild.
|
||||
*/
|
||||
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import AI from "../lib/ai.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(
|
||||
_: Client,
|
||||
interaction: ChatInputCommandInteraction,
|
||||
controller: AI
|
||||
): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = interaction.options.getString("prompt", true);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
// Test Llama connection
|
||||
const isConnected = await controller.llama.testConnection();
|
||||
if (!isConnected) {
|
||||
await interaction.editReply(replies.connectionError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create conversation
|
||||
const conversation = controller.getConversation(interaction.guildId, interaction.user.id);
|
||||
|
||||
// Clear stale conversations
|
||||
if (conversation.isStale()) {
|
||||
conversation.clear();
|
||||
}
|
||||
|
||||
// Add user message to conversation
|
||||
conversation.addUserMessage(prompt);
|
||||
|
||||
// Send to Llama
|
||||
const response = await controller.llama.chat(conversation.getHistory());
|
||||
|
||||
// Add assistant response to conversation
|
||||
conversation.addAssistantMessage(response);
|
||||
|
||||
// Reply with the response
|
||||
await interaction.editReply(replies.response(response));
|
||||
} catch (error) {
|
||||
logger.error("Error in ask command", {
|
||||
error,
|
||||
userId: interaction.user.id,
|
||||
guildId: interaction.guildId
|
||||
});
|
||||
await interaction.editReply(replies.error());
|
||||
}
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("ask")
|
||||
.setDescription("Ask a question to the AI assistant")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("prompt")
|
||||
.setDescription("Your question or prompt")
|
||||
.setRequired(true)
|
||||
.setMaxLength(2000)
|
||||
)
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* /askclear
|
||||
*
|
||||
* Clears the conversation history for the current user in the current guild.
|
||||
*/
|
||||
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
import Client from "$lib/client.js";
|
||||
import logger from "$lib/logger.js";
|
||||
|
||||
import AI from "../lib/ai.js";
|
||||
import replies from "../lib/replies.js";
|
||||
|
||||
async function execute(
|
||||
_: Client,
|
||||
interaction: ChatInputCommandInteraction,
|
||||
controller: AI
|
||||
): Promise<void> {
|
||||
if (!interaction.isChatInputCommand() || !interaction.inCachedGuild()) {
|
||||
logger.warn("Interaction rejected.", { interaction });
|
||||
return;
|
||||
}
|
||||
|
||||
controller.clearConversation(interaction.guildId, interaction.user.id);
|
||||
|
||||
await interaction.reply(replies.historyCleared());
|
||||
}
|
||||
|
||||
const data = new SlashCommandBuilder()
|
||||
.setName("askclear")
|
||||
.setDescription("Clear your conversation history with the AI assistant")
|
||||
.setDMPermission(false);
|
||||
|
||||
export { data, execute };
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Collection } from "discord.js";
|
||||
|
||||
import { Conversation } from "./conversation.js";
|
||||
import { LlamaClient } from "./llama.js";
|
||||
|
||||
/**
|
||||
* AI module controller.
|
||||
*
|
||||
* Manages conversations and Llama LLM interactions.
|
||||
*/
|
||||
export default class AI {
|
||||
/**
|
||||
* Llama API client.
|
||||
*/
|
||||
public readonly llama = new LlamaClient();
|
||||
|
||||
/**
|
||||
* Guild + User ID to Conversation mapping.
|
||||
*/
|
||||
private conversations_ = new Collection<string, Conversation>();
|
||||
|
||||
/**
|
||||
* Gets or creates a conversation for a user in a guild.
|
||||
*
|
||||
* @param guildId - Discord guild ID
|
||||
* @param userId - Discord user ID
|
||||
* @returns Conversation instance
|
||||
*/
|
||||
public getConversation(guildId: string, userId: string): Conversation {
|
||||
const key = `${guildId}:${userId}`;
|
||||
|
||||
if (!this.conversations_.has(key)) {
|
||||
this.conversations_.set(key, new Conversation(guildId, userId));
|
||||
}
|
||||
|
||||
return this.conversations_.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears conversation history for a user in a guild.
|
||||
*
|
||||
* @param guildId - Discord guild ID
|
||||
* @param userId - Discord user ID
|
||||
*/
|
||||
public clearConversation(guildId: string, userId: string): void {
|
||||
const key = `${guildId}:${userId}`;
|
||||
this.conversations_.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { LlamaMessage } from "./llama.js";
|
||||
|
||||
/**
|
||||
* Manages conversation history for a specific user in a guild.
|
||||
*/
|
||||
export class Conversation {
|
||||
public readonly guildId: string;
|
||||
public readonly userId: string;
|
||||
public readonly messages: LlamaMessage[] = [];
|
||||
public readonly createdAt: Date = new Date();
|
||||
public lastActivity: Date = new Date();
|
||||
|
||||
private readonly maxMessages = 50; // Limit conversation history
|
||||
|
||||
public constructor(guildId: string, userId: string) {
|
||||
this.guildId = guildId;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a user message to the conversation.
|
||||
*
|
||||
* @param content - User's message content
|
||||
*/
|
||||
public addUserMessage(content: string): void {
|
||||
this.messages.push({ role: "user", content });
|
||||
this.lastActivity = new Date();
|
||||
this.trimHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an assistant response to the conversation.
|
||||
*
|
||||
* @param content - Assistant's response content
|
||||
*/
|
||||
public addAssistantMessage(content: string): void {
|
||||
this.messages.push({ role: "assistant", content });
|
||||
this.lastActivity = new Date();
|
||||
this.trimHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the conversation history.
|
||||
*
|
||||
* @returns Array of messages
|
||||
*/
|
||||
public getHistory(): LlamaMessage[] {
|
||||
return [...this.messages];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the conversation history.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.messages.length = 0;
|
||||
this.lastActivity = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims conversation history to stay within limits.
|
||||
*/
|
||||
private trimHistory(): void {
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages.splice(0, this.messages.length - this.maxMessages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the conversation is stale (inactive for over 1 hour).
|
||||
*
|
||||
* @returns True if conversation should be considered stale
|
||||
*/
|
||||
public isStale(): boolean {
|
||||
const oneHour = 60 * 60 * 1000;
|
||||
return Date.now() - this.lastActivity.getTime() > oneHour;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import logger from "$lib/logger.js";
|
||||
import config from "$config.js";
|
||||
|
||||
export interface LlamaResponse {
|
||||
content: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export interface LlamaMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client for communicating with locally deployed Llama LLM.
|
||||
*/
|
||||
export class LlamaClient {
|
||||
private readonly baseUrl = config.llama.apiUrl || "http://localhost:11434";
|
||||
private readonly model = config.llama.model || "llama3.2";
|
||||
|
||||
/**
|
||||
* Sends a chat completion request to Llama.
|
||||
*
|
||||
* @param messages - Array of conversation messages
|
||||
* @returns Promise resolving to Llama response
|
||||
*/
|
||||
public async chat(messages: LlamaMessage[]): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
messages,
|
||||
stream: false
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Llama API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { message: { content: string } };
|
||||
return data.message.content;
|
||||
} catch (error) {
|
||||
logger.error("Failed to communicate with Llama API", { error });
|
||||
throw new Error("Unable to process your request. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests connection to Llama API.
|
||||
*
|
||||
* @returns Promise resolving to true if connection is successful
|
||||
*/
|
||||
public async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { BaseMessageOptions, EmbedBuilder, italic } from "discord.js";
|
||||
|
||||
import config from "$config.js";
|
||||
|
||||
const replies = {
|
||||
response: (content: string): BaseMessageOptions => {
|
||||
return { content };
|
||||
},
|
||||
|
||||
error: (
|
||||
message: string = "Sorry, I encountered an error processing your request."
|
||||
): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.error || "#ff0000")
|
||||
.setTitle("⚠️ Error")
|
||||
.setDescription(italic(message));
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
|
||||
connectionError: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.warning || "#ffaa00")
|
||||
.setTitle("🔌 Connection Issue")
|
||||
.setDescription(
|
||||
italic("Unable to connect to the LLAMA service. Please try again later.")
|
||||
);
|
||||
|
||||
return { embeds: [embed] };
|
||||
},
|
||||
|
||||
historyCleared: (): BaseMessageOptions => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(config.colors.success || "#00ff00")
|
||||
.setTitle("🧹 History Cleared")
|
||||
.setDescription(italic("Your conversation history has been cleared."));
|
||||
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
};
|
||||
|
||||
export default replies;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* AI module definition.
|
||||
*
|
||||
* Module to provide AI conversational capabilities using locally deployed Llama LLM.
|
||||
* Supports slash commands and reply-based interactions with conversation persistence.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import AI from "./lib/ai.js";
|
||||
|
||||
const name = "ai";
|
||||
const description = "AI conversational module with Llama LLM integration";
|
||||
|
||||
const controller = new AI();
|
||||
|
||||
export { controller, description, name };
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"$config.js": ["../../config.js"],
|
||||
"$lib/*": ["../../src/lib/*"],
|
||||
"$mlib/*": ["./lib/*"]
|
||||
},
|
||||
|
||||
"rootDir": ".",
|
||||
"outDir": "../../dist/modules/ai",
|
||||
|
||||
"composite": true
|
||||
},
|
||||
"references": [{ "path": "../../src/" }]
|
||||
}
|
||||
+5
-1
@@ -17,5 +17,9 @@
|
||||
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"references": [{ "path": "./modules/core" }, { "path": "./modules/music" }]
|
||||
"references": [
|
||||
{ "path": "./modules/core" },
|
||||
{ "path": "./modules/music" },
|
||||
{ "path": "./modules/ai" }
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user