Skip to content

Commit

Permalink
Implement AI provider architecture with OpenAI and Anthropic support
Browse files Browse the repository at this point in the history
  • Loading branch information
nukeop committed Feb 15, 2025
1 parent 1b56708 commit e2fc761
Show file tree
Hide file tree
Showing 8 changed files with 614 additions and 49 deletions.
498 changes: 486 additions & 12 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
},
"homepage": "https://github.com/NuclearPlayer/nuki-discord-bot#readme",
"dependencies": {
"@ai-sdk/anthropic": "^1.1.8",
"@ai-sdk/openai": "^1.1.11",
"discord-api-types": "^0.37.36",
"discord.js": "^14.8.0",
"dotenv": "^16.0.3",
Expand All @@ -35,6 +37,7 @@
"@types/lodash": "^4.14.191",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"ai": "^4.1.40",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
Expand Down
38 changes: 38 additions & 0 deletions src/ai/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AnthropicServiceProvider } from './providers/anthropic-provider';
import { OpenAIServiceProvider } from './providers/openai-provider';
import { AIModelProvider, AIProvider } from './types';

export class AIFactory {
private static instance: AIFactory;
private currentProvider: AIProvider;
private providers: Map<AIModelProvider, AIProvider>;

private constructor() {
this.providers = new Map();
this.providers.set(AIModelProvider.OPENAI, new OpenAIServiceProvider());
this.providers.set(
AIModelProvider.ANTHROPIC,
new AnthropicServiceProvider(),
);
this.currentProvider = this.providers.get(AIModelProvider.ANTHROPIC)!;
}

static getInstance(): AIFactory {
if (!AIFactory.instance) {
AIFactory.instance = new AIFactory();
}
return AIFactory.instance;
}

setProvider(provider: AIModelProvider): void {
const newProvider = this.providers.get(provider);
if (!newProvider) {
throw new Error(`Provider ${provider} not found`);
}
this.currentProvider = newProvider;
}

getCurrentProvider(): AIProvider {
return this.currentProvider;
}
}
17 changes: 17 additions & 0 deletions src/ai/providers/anthropic-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AIProvider } from '../types';
import { anthropic } from '@ai-sdk/anthropic';
import { CoreMessage, generateText } from 'ai';

export class AnthropicServiceProvider implements AIProvider {
constructor() {}

async generateResponse(messages: CoreMessage[]): Promise<string> {
const response = await generateText({
model: anthropic('claude-3-5-sonnet-latest'),
maxTokens: 256,
messages,
});

return response.text;
}
}
17 changes: 17 additions & 0 deletions src/ai/providers/openai-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AIProvider } from '../types';
import { openai } from '@ai-sdk/openai';
import { CoreMessage, generateText } from 'ai';

export class OpenAIServiceProvider implements AIProvider {
constructor() {}

async generateResponse(messages: CoreMessage[]): Promise<string> {
const response = await generateText({
model: openai('gpt-4o'),
messages,
maxTokens: 256,
});

return response.text;
}
}
10 changes: 10 additions & 0 deletions src/ai/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CoreMessage } from 'ai';

export interface AIProvider {
generateResponse(messages: CoreMessage[]): Promise<string>;
}

export enum AIModelProvider {
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
}
47 changes: 20 additions & 27 deletions src/directives/chatbot.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { AIFactory } from '../ai/factory';
import Logger from '../logger';
import { OpenAiApiService } from '../open-ai-api-service';
import { createAssistantMessage, createUserMessage } from '../open-ai-tools';
import { PromptBuilder } from '../prompt-builder';
import { CoreMessage, UserContent } from 'ai';
import { Client, Message } from 'discord.js';
import { random } from 'lodash';
import {
ChatCompletionContentPart,
ChatCompletionMessageParam,
} from 'openai/resources/chat/completions';

export const chatbot = {
execute: async (client: Client, message: Message) => {
Expand All @@ -19,7 +16,6 @@ export const chatbot = {
!message.system
) {
await message.channel.sendTyping();
const openAiService = new OpenAiApiService();
const availableEmoji = message.guild?.emojis.cache.map((emoji) => ({
id: emoji.id,
name: emoji.name ?? '',
Expand All @@ -32,7 +28,7 @@ export const chatbot = {
.withCustomEmoji(availableEmoji)
.withChatbotFooter()
.build();
const lastMessages: ChatCompletionMessageParam[] = (
const lastMessages: CoreMessage[] = (
await Promise.all(
(
await message.channel.messages.fetch({ limit: 30 })
Expand All @@ -48,7 +44,7 @@ export const chatbot = {
message.createdTimestamp,
).toLocaleString()}]: ${message.content}`,
},
],
] satisfies CoreMessage['content'],
});
}

Expand All @@ -59,7 +55,7 @@ export const chatbot = {

const image = message.attachments.first()?.url;

let content: ChatCompletionContentPart[] = [
let content: CoreMessage['content'] = [
{
type: 'text',
text: `[${new Date(
Expand All @@ -81,43 +77,40 @@ export const chatbot = {
text: `[id:${message.author.id}]:${content}`,
},
{
type: 'image_url',
image_url: {
detail: 'low',
url: image,
},
type: 'image',
image,
},
] as ChatCompletionContentPart[];
] satisfies CoreMessage['content'];
}

return createUserMessage({
content,
content: content as UserContent,
name: serverNickname.length > 0 ? serverNickname : undefined,
});
}),
)
).reverse();

Logger.info('Querying OpenAI API...');
const messageToSend = await openAiService
.getClient()
.chat.completions.create({
max_tokens: 256,
model: 'gpt-4o-2024-08-06',
messages: [{ role: 'system', content: prompt }, ...lastMessages],
});
Logger.info('Querying AI API...');
const aiFactory = AIFactory.getInstance();
const response = await aiFactory
.getCurrentProvider()
.generateResponse([
{ role: 'system', content: prompt },
...lastMessages,
]);

await message.channel.send(
messageToSend.choices[0].message?.content
?.replace('Nuki:', '')
response
.replace('Nuki:', '')
.replace('Nuki[id:1087848070512910336]:', '')
.replace(
new RegExp(
`\\[\\d{1,2}\\/\\d{1,2}\\/\\d{4}, \\d{1,2}:\\d{1,2}:\\d{1,2} [AP]M\\] ?`,
'g',
),
'',
)!,
),
);
}
},
Expand Down
33 changes: 23 additions & 10 deletions src/open-ai-tools.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { ChatCompletionAssistantMessageParam, ChatCompletionContentPart, ChatCompletionContentPartText, ChatCompletionUserMessageParam } from 'openai/resources';
import {
AssistantContent,
CoreAssistantMessage,
CoreMessage,
CoreUserMessage,
UserContent,
} from 'ai';

export const createUserMessage = ({content, name}: {content: Array<ChatCompletionContentPart>, name?: string}): ChatCompletionUserMessageParam => {
export const createUserMessage = ({
content,
}: {
content: UserContent;
name?: string;
}): CoreUserMessage => {
return {
role: 'user',
content,
name
}
}
content,
};
};

export const createAssistantMessage = ({content}: {content: Array<ChatCompletionContentPartText>}): ChatCompletionAssistantMessageParam => {
export const createAssistantMessage = ({
content,
}: {
content: AssistantContent;
}): CoreAssistantMessage => {
return {
role: 'assistant',
content,
name: 'Nuki'
}
}
};
};

0 comments on commit e2fc761

Please sign in to comment.