feat: Add AI module

ollama based ai module for more bot entertaiment
This commit is contained in:
deytenit committed 2025-06-13 14:42:08 +03:00
1 parent 63e4d85cbb
commit 534cd72444
10 files changed
+394 -2

No files matched your search

+8 -1
View File
@@ -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;
+79
View File
@@ -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 };
+35
View File
@@ -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 };
+49
View File
@@ -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);
}
}
+77
View File
@@ -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;
}
}
+66
View File
@@ -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;
}
}
}
+42
View File
@@ -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;
+17
View File
@@ -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 };
+16
View File
@@ -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
View File
@@ -17,5 +17,9 @@
"skipLibCheck": true
},
"references": [{ "path": "./modules/core" }, { "path": "./modules/music" }]
"references": [
{ "path": "./modules/core" },
{ "path": "./modules/music" },
{ "path": "./modules/ai" }
]
}