Skip to content

Added grok mode #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use nix
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"bufferutil": "^4.0.8",
"dayjs": "^1.11.13",
"discord.js": "^14.16.3",
"fastest-levenshtein": "^1.0.16",
"groq-sdk": "^0.9.1",
"human-interval": "^2.0.1",
"node-cron": "^3.0.3",
Expand Down
11 changes: 11 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
let
pkgs = import <nixpkgs> {};
in
pkgs.mkShell {
buildInputs = with pkgs; [
bun
corepack_latest
nodejs_latest
eslint
];
}
97 changes: 3 additions & 94 deletions src/commands/summarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ import {
type ChatInputCommandInteraction,
type PublicThreadChannel,
} from "discord.js";
import Groq from "groq-sdk";
import human from "human-interval";

import { env } from "../env";

const groq = new Groq({ apiKey: env.GROQ_API_KEY });
import { summarize } from "../utils/summarization";

dayjs.extend(relativeTime);

Expand All @@ -37,105 +32,19 @@ export async function command(interaction: ChatInputCommandInteraction) {
const { options } = interaction;

const topic = options.getString("topic");
const timeframe = options.getString("timeframe") ?? "1 hour";
const timeframe = options.getString("timeframe");

if (!topic) {
await interaction.reply("Please provide a topic to summarize");
return;
}

const timeframeMs = human(timeframe);

if (!timeframeMs) {
await interaction.reply("Invalid timeframe provided");
return;
}

const date = new Date(Date.now() - timeframeMs);
const formatted = dayjs(date).fromNow();

await interaction.reply({
content: `Summarizing messages related to ${topic} from ${formatted}.`,
flags: MessageFlags.Ephemeral,
});

const isChannel = interaction.channel;

if (!isChannel) {
await interaction.reply("This command can only be used in a channel");
return;
}

const snowflake = (
(BigInt(date.valueOf()) - BigInt(1420070400000)) <<
BigInt(22)
).toString();

const messages = await interaction.channel.messages.fetch({
limit: 100,
after: snowflake,
});

const corpus = messages
.reverse()
.map(
(message) =>
`[${message.author.displayName} ${new Date(message.createdTimestamp).toISOString()}] ${message.content}`,
)
.join("\n");

const content = `
${corpus}


Using the above message corpus, generate a bulleted summary of anything relevant to the following topic: **${topic}**. Mention specific things people said and anything useful to document. Pull all details relevant to ${topic}.

When reading a message, the first part is the username and the second part is the timestamp. For example, [User A 2021-08-01T00:00:00.000Z].

Avoid pinging users, only use their username (e.g. Ray said ...). Follow all markdown rules relevant to Discord.

Use an analytical tone. Include relevant details. For example, "User A mentioned that they were going to the store. User B responded with a question about the store's location."
Include as much detail as possible. At the end summarize any conclusions or decisions made.
`.trim();

const response = await groq.chat.completions.create({
messages: [
{
role: "user",
content,
},
],
model: "llama-3.3-70b-versatile",
});

const thread = (await interaction.channel.threads.create({
name: `Summary of ${topic} from ${formatted}`,
autoArchiveDuration: 60,
reason: `Summarizing messages related to ${topic} from ${formatted}.`,
})) as PublicThreadChannel<false>;

const message = response.choices[0].message;

if (!message.content) {
console.error("No content");
await thread.send("Error: No content");
return;
}

if (message.content.length > 2000) {
const chunks = message.content.match(/[\s\S]{1,2000}/g);

if (!chunks) {
console.error("No chunks");
await thread.send("Error: No chunks");
return;
}

for (const chunk of chunks) {
console.log(chunk);
await thread.send(chunk);
}
} else {
await thread.send(message.content);
}
await summarize(timeframe, topic, interaction);
}
35 changes: 35 additions & 0 deletions src/events/message_create/grok.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { TextChannel, type Message } from "discord.js";
import { distance } from "fastest-levenshtein";
import { summarize } from "../../utils/summarization";

const ASSISTANT_TRIGGER = "grok";
const SUMMARIZE_TRIGGER = "summary";

export default async function handler(message: Message) {
if (message.author.bot) return;
if (message.channel.isDMBased()) return;
if (!(message.channel instanceof TextChannel)) return;

// message.startThread(options);

const result = message.content.replace(/\s+/g, " ").trim();
const parts = result.split(" ");

if (parts.length < 2 || !parts[0].startsWith("@")) return;

const [ref, invocation, ...time1] = parts;

const isThisReal = message.content.match(/is\s+this\s+real/);
const time = isThisReal ? time1.slice(2) : time1;

const refs = ref.substring(1);
if (distance(ASSISTANT_TRIGGER, refs) > 3) return;

if (distance(SUMMARIZE_TRIGGER, invocation) > 5 && !isThisReal) return;

await summarize(
time.length > 0 ? time.join(" ") : null,
null,
message as Message<true>,
);
}
2 changes: 2 additions & 0 deletions src/events/message_create/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dashboard from "./dashboard";
import evergreenIt from "./evergreen-it";
import voiceMessageTranscription from "./voice-transcription";
import welcomer from "./welcomer";
import grok from "./grok";

export const eventType = Events.MessageCreate;
export {
Expand All @@ -13,4 +14,5 @@ export {
evergreenIt,
voiceMessageTranscription,
welcomer,
grok,
};
118 changes: 118 additions & 0 deletions src/utils/summarization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import dayjs from "dayjs";
import {
ChatInputCommandInteraction,
Message,
MessageFlags,
MessagePayload,
TextChannel,
type GuildTextBasedChannel,
type MessageReplyOptions,
type OmitPartialGroupDMChannel,
type PublicThreadChannel,
type TextBasedChannel,
} from "discord.js";
import human from "human-interval";
import Groq from "groq-sdk";
import { env } from "../env";
const groq = new Groq({ apiKey: env.GROQ_API_KEY });

export async function summarize(
timeframe: string | null,
top: string | null,
replyable: Message<true> | ChatInputCommandInteraction,
) {
const timeframeMs = human(timeframe ?? "1 hour");
const topic =
top || "whatever the most common theme of the previous messages is";
const displayTopic = top || "WHATEVER";

if (!timeframeMs) {
await replyable.reply("Invalid timeframe provided");
return;
}

const date = new Date(Date.now() - timeframeMs);
const formatted = dayjs(date).fromNow();

if (replyable instanceof ChatInputCommandInteraction) {
await replyable.reply({
content: `Summarizing messages related to ${topic} from ${formatted}.`,
flags: MessageFlags.Ephemeral,
});
}

const snowflake = (
(BigInt(date.valueOf()) - BigInt(1420070400000)) <<
BigInt(22)
).toString();

const channel = replyable.channel as TextChannel;

const messages = await channel.messages.fetch({
limit: 100,
after: snowflake,
});

const corpus = messages
.reverse()
.map(
(message) =>
`[${message.author.displayName} ${new Date(message.createdTimestamp).toISOString()}] ${message.content}`,
)
.join("\n");

const content = `
${corpus}


Using the above message corpus, generate a bulleted summary of anything relevant to the following topic: **${topic}**. Mention specific things people said and anything useful to document. Pull all details relevant to ${topic}.

When reading a message, the first part is the username and the second part is the timestamp. For example, [User A 2021-08-01T00:00:00.000Z].

Avoid pinging users, only use their username (e.g. Ray said ...). Follow all markdown rules relevant to Discord.

Use an analytical tone. Include relevant details. For example, "User A mentioned that they were going to the store. User B responded with a question about the store's location."
Include as much detail as possible. At the end summarize any conclusions or decisions made.
`.trim();

const response = await groq.chat.completions.create({
messages: [
{
role: "user",
content,
},
],
model: "llama-3.3-70b-versatile",
});

const thread = (await channel.threads.create({
name: `Summary of ${displayTopic} from ${formatted}`,
autoArchiveDuration: 60,
reason: `Summarizing messages related to ${displayTopic} from ${formatted}.`,
})) as PublicThreadChannel<false>;

const message = response.choices[0].message;

if (!message.content) {
console.error("No content");
await thread.send("Error: No content");
return;
}

if (message.content.length > 2000) {
const chunks = message.content.match(/[\s\S]{1,2000}/g);

if (!chunks) {
console.error("No chunks");
await thread.send("Error: No chunks");
return;
}

for (const chunk of chunks) {
console.log(chunk);
await thread.send(chunk);
}
} else {
await thread.send(message.content);
}
}