177 lines
5.2 KiB
TypeScript
177 lines
5.2 KiB
TypeScript
import {
|
|
OllamaRequest,
|
|
OllamaResponse,
|
|
NewStatusBody,
|
|
Notification,
|
|
OllamaConfigOptions,
|
|
} from "../types.js";
|
|
import striptags from "striptags";
|
|
import { PrismaClient } from "../generated/prisma/client.js";
|
|
import {
|
|
getInstanceEmojis,
|
|
deleteNotification,
|
|
getNotifications,
|
|
} from "./api.js";
|
|
import { storeUserData, storePromptData } from "./prisma.js";
|
|
import {
|
|
isFromWhitelistedDomain,
|
|
alreadyRespondedTo,
|
|
recordPendingResponse,
|
|
trimInputData,
|
|
selectRandomEmoji,
|
|
} from "./util.js";
|
|
|
|
export const prisma = new PrismaClient();
|
|
|
|
export const envConfig = {
|
|
pleromaInstanceUrl: process.env.PLEROMA_INSTANCE_URL || "",
|
|
pleromaInstanceDomain: process.env.PLEROMA_INSTANCE_DOMAIN || "",
|
|
whitelistOnly: process.env.ONLY_WHITELIST === "true" ? true : false,
|
|
whitelistedDomains: process.env.WHITELISTED_DOMAINS
|
|
? process.env.WHITELISTED_DOMAINS.split(",")
|
|
: [process.env.PLEROMA_INSTANCE_DOMAIN],
|
|
ollamaUrl: process.env.OLLAMA_URL || "",
|
|
ollamaSystemPrompt:
|
|
process.env.OLLAMA_SYSTEM_PROMPT ||
|
|
"You are a helpful AI assistant. Answer all questions concisely.",
|
|
ollamaModel: process.env.OLLAMA_MODEL || "",
|
|
fetchInterval: process.env.FETCH_INTERVAL
|
|
? parseInt(process.env.FETCH_INTERVAL)
|
|
: 15000,
|
|
bearerToken: process.env.INSTANCE_BEARER_TOKEN || "",
|
|
};
|
|
|
|
const ollamaConfig: OllamaConfigOptions = {
|
|
temperature: 1.4,
|
|
top_k: 100,
|
|
top_p: 0.8,
|
|
};
|
|
|
|
// this could be helpful
|
|
// https://replicate.com/blog/how-to-prompt-llama
|
|
|
|
const generateOllamaRequest = async (
|
|
notification: Notification
|
|
): Promise<OllamaResponse | undefined> => {
|
|
const { whitelistOnly, ollamaModel, ollamaSystemPrompt, ollamaUrl } =
|
|
envConfig;
|
|
try {
|
|
if (
|
|
striptags(notification.status.content).includes("!prompt") &&
|
|
!notification.status.account.bot && // sanity check, sort of
|
|
notification.type === "mention" &&
|
|
notification.status.visibility !== "private" // for safety, let's only respond to public messages
|
|
) {
|
|
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
|
await deleteNotification(notification);
|
|
return;
|
|
}
|
|
if (await alreadyRespondedTo(notification)) {
|
|
return;
|
|
}
|
|
await recordPendingResponse(notification);
|
|
await storeUserData(notification);
|
|
const ollamaRequestBody: OllamaRequest = {
|
|
model: ollamaModel,
|
|
system: ollamaSystemPrompt,
|
|
prompt: `[INST] @${
|
|
notification.status.account.fqn
|
|
} says: ${trimInputData(notification.status.content)} [/INST]`,
|
|
stream: false,
|
|
options: ollamaConfig,
|
|
};
|
|
const response = await fetch(`${ollamaUrl}/api/generate`, {
|
|
method: "POST",
|
|
body: JSON.stringify(ollamaRequestBody),
|
|
});
|
|
const ollamaResponse: OllamaResponse = await response.json();
|
|
await storePromptData(notification, ollamaResponse);
|
|
return ollamaResponse;
|
|
}
|
|
} catch (error: any) {
|
|
throw new Error(error.message);
|
|
}
|
|
};
|
|
|
|
const postReplyToStatus = async (
|
|
notification: Notification,
|
|
ollamaResponseBody: OllamaResponse
|
|
) => {
|
|
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
|
const emojiList = await getInstanceEmojis();
|
|
let randomEmoji;
|
|
if (emojiList) {
|
|
randomEmoji = selectRandomEmoji(emojiList);
|
|
}
|
|
try {
|
|
let mentions: string[];
|
|
const statusBody: NewStatusBody = {
|
|
content_type: "text/markdown",
|
|
status: `${ollamaResponseBody.response} :${randomEmoji}:`,
|
|
in_reply_to_id: notification.status.id,
|
|
};
|
|
if (
|
|
notification.status.mentions &&
|
|
notification.status.mentions.length > 0
|
|
) {
|
|
mentions = notification.status.mentions.map((mention) => {
|
|
return mention.acct;
|
|
});
|
|
statusBody.to = mentions;
|
|
}
|
|
|
|
const response = await fetch(`${pleromaInstanceUrl}/api/v1/statuses`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${bearerToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(statusBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`New status request failed: ${response.statusText}`);
|
|
}
|
|
|
|
await deleteNotification(notification);
|
|
} catch (error: any) {
|
|
throw new Error(error.message);
|
|
}
|
|
};
|
|
|
|
let notifications = [];
|
|
const beginFetchCycle = async () => {
|
|
setInterval(async () => {
|
|
notifications = await getNotifications();
|
|
if (notifications.length > 0) {
|
|
await Promise.all(
|
|
notifications.map(async (notification) => {
|
|
try {
|
|
const ollamaResponse = await generateOllamaRequest(notification);
|
|
if (ollamaResponse) {
|
|
postReplyToStatus(notification, ollamaResponse);
|
|
}
|
|
} catch (error: any) {
|
|
throw new Error(error.message);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
}, envConfig.fetchInterval); // lower intervals may cause the bot to respond multiple times to the same message, but we try to mitigate this with the deleteNotification function
|
|
};
|
|
|
|
console.log(
|
|
`Fetching notifications from ${envConfig.pleromaInstanceDomain}, every ${
|
|
envConfig.fetchInterval / 1000
|
|
} seconds.`
|
|
);
|
|
console.log(
|
|
`Accepting prompts from: ${envConfig.whitelistedDomains.join(", ")}`
|
|
);
|
|
console.log(
|
|
`Using model: ${envConfig.ollamaModel}\nConfig: ${JSON.stringify(
|
|
ollamaConfig
|
|
)}`
|
|
);
|
|
await beginFetchCycle();
|