-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
connect /ask command to chatgpt api via npm:ai
- Loading branch information
Showing
8 changed files
with
251 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,67 @@ | ||
import type { ChatInputCommandInteraction } from "discord.js"; | ||
import { openai } from "@ai-sdk/openai"; | ||
import { generateText, streamText } from "ai"; | ||
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; | ||
import type { CommandCollection } from "./types"; | ||
|
||
export type CommandCollection = Record< | ||
string, | ||
{ | ||
description: string; | ||
handler: (interaction: ChatInputCommandInteraction) => Promise<void>; | ||
} | ||
>; | ||
|
||
export const commands = { | ||
export const commands: CommandCollection = { | ||
healthcheck: { | ||
description: "Check if the bot is healthy.", | ||
builder: new SlashCommandBuilder().setDescription("Check if the bot is healthy."), | ||
async handler(interaction) { | ||
await interaction.reply("I'm healthy!"); | ||
}, | ||
}, | ||
} satisfies CommandCollection; | ||
ask: { | ||
builder: new SlashCommandBuilder() | ||
.setDescription("Ask a question.") | ||
.addStringOption((option) => | ||
option.setName("question").setDescription("The question to ask.").setRequired(true), | ||
) | ||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) | ||
.setDMPermission(true), | ||
async handler(interaction) { | ||
const question = interaction.options.getString("question"); | ||
await interaction.deferReply(); | ||
|
||
if (question) { | ||
try { | ||
const answer = await streamText({ | ||
model: openai("gpt-4o"), | ||
system: | ||
"You are a helpful assistant. Never return a response more than 1500 characters", | ||
maxTokens: 512, | ||
prompt: question, | ||
}); | ||
|
||
let content = ""; | ||
let pendingChunk = ""; | ||
for await (const chunk of answer.textStream) { | ||
if (pendingChunk.length + chunk.length < 100) { | ||
pendingChunk = pendingChunk + chunk; | ||
continue; | ||
} | ||
|
||
if (content.length + pendingChunk.length < 2000) { | ||
content = content + pendingChunk; | ||
pendingChunk = ""; | ||
await interaction.editReply({ content }); | ||
} else { | ||
content = pendingChunk = chunk; | ||
await interaction.followUp({ content }); | ||
} | ||
} | ||
|
||
if (content.length + pendingChunk.length < 2000) { | ||
content = content + pendingChunk; | ||
pendingChunk = ""; | ||
await interaction.editReply({ content }); | ||
} else { | ||
content = pendingChunk; | ||
await interaction.followUp({ content }); | ||
} | ||
} catch (error) { | ||
console.error(error); | ||
} | ||
} | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,24 @@ | ||
{ | ||
"name": "@kampus/llm-bot", | ||
"module": "index.ts", | ||
"type": "module", | ||
"scripts": { | ||
"deploy": "bun ./bin/deploy.ts", | ||
"start": "bun ./bin/start.ts" | ||
}, | ||
"devDependencies": { | ||
"@types/bun": "latest", | ||
"@types/debug": "^4.1.12" | ||
}, | ||
"peerDependencies": { | ||
"typescript": "^5.0.0" | ||
}, | ||
"dependencies": { | ||
"debug": "^4.3.4", | ||
"discord.js": "^14.15.2", | ||
"znv": "^0.4.0", | ||
"zod": "^3.23.8" | ||
} | ||
"name": "@kampus/llm-bot", | ||
"module": "index.ts", | ||
"type": "module", | ||
"scripts": { | ||
"deploy": "bun ./bin/deploy.ts", | ||
"start": "bun ./bin/start.ts" | ||
}, | ||
"devDependencies": { | ||
"@types/bun": "latest", | ||
"@types/debug": "^4.1.12" | ||
}, | ||
"peerDependencies": { | ||
"typescript": "^5.0.0" | ||
}, | ||
"dependencies": { | ||
"@ai-sdk/openai": "^0.0.18", | ||
"ai": "^3.1.22", | ||
"debug": "^4.3.4", | ||
"discord.js": "^14.15.2", | ||
"znv": "^0.4.0", | ||
"zod": "^3.23.8" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/** | ||
* TextEncoderStream polyfill based on Node.js' implementation https://github.com/nodejs/node/blob/3f3226c8e363a5f06c1e6a37abd59b6b8c1923f1/lib/internal/webstreams/encoding.js#L38-L119 (MIT License) | ||
*/ | ||
export class TextEncoderStream { | ||
#pendingHighSurrogate: string | null = null; | ||
|
||
#handle = new TextEncoder(); | ||
|
||
#transform = new TransformStream<string, Uint8Array>({ | ||
transform: (chunk, controller) => { | ||
// https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk | ||
// biome-ignore lint/style/noParameterAssign: This is a polyfill | ||
chunk = String(chunk); | ||
|
||
let finalChunk = ""; | ||
for (const item of chunk) { | ||
const codeUnit = item.charCodeAt(0); | ||
if (this.#pendingHighSurrogate !== null) { | ||
const highSurrogate = this.#pendingHighSurrogate; | ||
|
||
this.#pendingHighSurrogate = null; | ||
if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { | ||
finalChunk += highSurrogate + item; | ||
continue; | ||
} | ||
|
||
finalChunk += "\uFFFD"; | ||
} | ||
|
||
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { | ||
this.#pendingHighSurrogate = item; | ||
continue; | ||
} | ||
|
||
if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) { | ||
finalChunk += "\uFFFD"; | ||
continue; | ||
} | ||
|
||
finalChunk += item; | ||
} | ||
|
||
if (finalChunk) { | ||
controller.enqueue(this.#handle.encode(finalChunk)); | ||
} | ||
}, | ||
|
||
flush: (controller) => { | ||
// https://encoding.spec.whatwg.org/#encode-and-flush | ||
if (this.#pendingHighSurrogate !== null) { | ||
controller.enqueue(new Uint8Array([0xef, 0xbf, 0xbd])); | ||
} | ||
}, | ||
}); | ||
|
||
get encoding() { | ||
return this.#handle.encoding; | ||
} | ||
|
||
get readable() { | ||
return this.#transform.readable; | ||
} | ||
|
||
get writable() { | ||
return this.#transform.writable; | ||
} | ||
|
||
get [Symbol.toStringTag]() { | ||
return "TextEncoderStream"; | ||
} | ||
} | ||
|
||
/** | ||
* TextDecoderStream polyfill based on Node.js' implementation https://github.com/nodejs/node/blob/3f3226c8e363a5f06c1e6a37abd59b6b8c1923f1/lib/internal/webstreams/encoding.js#L121-L200 (MIT License) | ||
*/ | ||
export class TextDecoderStream { | ||
#handle: TextDecoder; | ||
|
||
#transform = new TransformStream({ | ||
transform: (chunk, controller) => { | ||
const value = this.#handle.decode(chunk, { stream: true }); | ||
|
||
if (value) { | ||
controller.enqueue(value); | ||
} | ||
}, | ||
flush: (controller) => { | ||
const value = this.#handle.decode(); | ||
if (value) { | ||
controller.enqueue(value); | ||
} | ||
|
||
controller.terminate(); | ||
}, | ||
}); | ||
|
||
constructor(encoding = "utf-8", options: TextDecoderOptions = {}) { | ||
this.#handle = new TextDecoder(encoding, options); | ||
} | ||
|
||
get encoding() { | ||
return this.#handle.encoding; | ||
} | ||
|
||
get fatal() { | ||
return this.#handle.fatal; | ||
} | ||
|
||
get ignoreBOM() { | ||
return this.#handle.ignoreBOM; | ||
} | ||
|
||
get readable() { | ||
return this.#transform.readable; | ||
} | ||
|
||
get writable() { | ||
return this.#transform.writable; | ||
} | ||
|
||
get [Symbol.toStringTag]() { | ||
return "TextDecoderStream"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { | ||
ChatInputCommandInteraction, | ||
SharedSlashCommand, | ||
SlashCommandBuilder, | ||
} from "discord.js"; | ||
|
||
/** | ||
* Defines the structure of a command | ||
*/ | ||
export interface Command { | ||
/** | ||
* Slash command builder to define the command | ||
* | ||
* @param builder - The builder to define the command | ||
*/ | ||
builder: SharedSlashCommand; | ||
/** | ||
* The function to execute when the command is called | ||
* | ||
* @param interaction - The interaction of the command | ||
*/ | ||
handler(interaction: ChatInputCommandInteraction): Promise<void> | void; | ||
} | ||
|
||
export type CommandCollection = Record<string, Command>; |