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
@@ -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;
|
||||
Reference in new issue
Block a user