Compare commits

...

2 Commits

Author SHA1 Message Date
ff5c7506ff refactor 2025-07-05 13:34:24 +00:00
5c51acc8d1 update dependencies, description 2025-07-05 12:51:00 +00:00
3 changed files with 61 additions and 44 deletions

13
package-lock.json generated
View File

@ -1,21 +1,21 @@
{ {
"name": "pleroma-ollama-bot", "name": "pleroma-ollama-bot",
"version": "1.0.0", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pleroma-ollama-bot", "name": "pleroma-ollama-bot",
"version": "1.0.0", "version": "1.0.5",
"dependencies": { "dependencies": {
"@prisma/client": "^6.10.1", "@prisma/client": "^6.10.1",
"@types/node": "^24.0.5",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"striptags": "^3.2.0", "striptags": "^3.2.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.10",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"prisma": "^6.10.1" "prisma": "^6.10.1"
} }
@ -164,10 +164,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.0.5", "version": "24.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
"integrity": "sha512-CXEG9E7GCTOZIre0WdDznmnhvF7xi7AmnP/zF496trmLiqlfdtxp9nPRgLVqfmJ8jgtcKcs0EcvOu2yDZSuvTg==", "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
"license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.8.0" "undici-types": "~7.8.0"
} }

View File

@ -9,16 +9,16 @@
"type": "module", "type": "module",
"keywords": [], "keywords": [],
"author": "NiceCrew", "author": "NiceCrew",
"description": "A simple bot that responds to activities from Pleroma instances using Ollama's API.", "description": "A simple bot that responds to activities from Pleroma instances using Ollama's API at a configurable interval.",
"dependencies": { "dependencies": {
"@prisma/client": "^6.10.1", "@prisma/client": "^6.10.1",
"@types/node": "^24.0.5",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"striptags": "^3.2.0", "striptags": "^3.2.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.10",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"prisma": "^6.10.1" "prisma": "^6.10.1"
} }

View File

@ -10,14 +10,35 @@ import { PrismaClient } from "../generated/prisma/client.js";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
const envConfig = {
pleromaInstanceUrl: process.env.PLEROMA_INSTANCE_URL || "",
pleromaInstanceDomain: process.env.PLEROMA_INSTANCE_DOMAIN || "",
onlyLocalReplies: process.env.ONLY_LOCAL_REPLIES === "true" ? true : false,
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: 0.3,
num_predict: 400,
};
const getNotifications = async () => { const getNotifications = async () => {
const { bearerToken, pleromaInstanceUrl } = envConfig;
try { try {
const request = await fetch( const request = await fetch(
`${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications?types[]=mention`, `${pleromaInstanceUrl}/api/v1/notifications?types[]=mention`,
{ {
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`, Authorization: `Bearer ${bearerToken}`,
}, },
} }
); );
@ -82,11 +103,12 @@ const storePromptData = async (
} }
}; };
const trimInputData = (input: string) => { const trimInputData = (input: string): string => {
const strippedInput = striptags(input); const strippedInput = striptags(input);
const split = strippedInput.split(" "); const split = strippedInput.split(" ");
const promptStringIndex = split.indexOf("!prompt"); const promptStringIndex = split.indexOf("!prompt");
return split.slice(promptStringIndex + 1).join(" "); // returns everything after the !prompt split.splice(promptStringIndex, 1);
return split.join(" "); // returns everything after the !prompt
}; };
const recordPendingResponse = async (notification: Notification) => { const recordPendingResponse = async (notification: Notification) => {
@ -104,6 +126,13 @@ const recordPendingResponse = async (notification: Notification) => {
const generateOllamaRequest = async ( const generateOllamaRequest = async (
notification: Notification notification: Notification
): Promise<OllamaResponse | undefined> => { ): Promise<OllamaResponse | undefined> => {
const {
onlyLocalReplies,
pleromaInstanceDomain,
ollamaModel,
ollamaSystemPrompt,
ollamaUrl,
} = envConfig;
try { try {
if ( if (
striptags(notification.status.content).includes("!prompt") && striptags(notification.status.content).includes("!prompt") &&
@ -111,10 +140,8 @@ const generateOllamaRequest = async (
notification.type === "mention" notification.type === "mention"
) { ) {
if ( if (
process.env.ONLY_LOCAL_REPLIES === "true" && onlyLocalReplies &&
!notification.status.account.fqn.includes( !notification.status.account.fqn.includes(`@${pleromaInstanceDomain}`)
`@${process.env.PLEROMA_INSTANCE_DOMAIN}`
)
) { ) {
return; return;
} }
@ -123,20 +150,16 @@ const generateOllamaRequest = async (
} }
await recordPendingResponse(notification); await recordPendingResponse(notification);
await storeUserData(notification); await storeUserData(notification);
const ollamaConfig: OllamaConfigOptions = {
temperature: 1.2,
num_predict: 400,
};
const ollamaRequestBody: OllamaRequest = { const ollamaRequestBody: OllamaRequest = {
model: process.env.OLLAMA_MODEL as string, model: ollamaModel,
system: process.env.OLLAMA_SYSTEM_PROMPT as string, system: ollamaSystemPrompt,
prompt: `@${notification.status.account.fqn} says: ${trimInputData( prompt: `@${notification.status.account.fqn} says: ${trimInputData(
notification.status.content notification.status.content
)}`, )}`,
stream: false, stream: false,
options: ollamaConfig, options: ollamaConfig,
}; };
const response = await fetch(`${process.env.OLLAMA_URL}/api/generate`, { const response = await fetch(`${ollamaUrl}/api/generate`, {
method: "POST", method: "POST",
body: JSON.stringify(ollamaRequestBody), body: JSON.stringify(ollamaRequestBody),
}); });
@ -153,6 +176,7 @@ const postReplyToStatus = async (
notification: Notification, notification: Notification,
ollamaResponseBody: OllamaResponse ollamaResponseBody: OllamaResponse
) => { ) => {
const { pleromaInstanceUrl, bearerToken } = envConfig;
try { try {
let mentions: string[]; let mentions: string[];
const statusBody: NewStatusBody = { const statusBody: NewStatusBody = {
@ -170,17 +194,14 @@ const postReplyToStatus = async (
statusBody.to = mentions; statusBody.to = mentions;
} }
const response = await fetch( const response = await fetch(`${pleromaInstanceUrl}/api/v1/statuses`, {
`${process.env.PLEROMA_INSTANCE_URL}/api/v1/statuses`,
{
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`, Authorization: `Bearer ${bearerToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(statusBody), body: JSON.stringify(statusBody),
} });
);
if (!response.ok) { if (!response.ok) {
throw new Error(`New status request failed: ${response.statusText}`); throw new Error(`New status request failed: ${response.statusText}`);
@ -193,6 +214,7 @@ const postReplyToStatus = async (
}; };
const deleteNotification = async (notification: Notification) => { const deleteNotification = async (notification: Notification) => {
const { pleromaInstanceUrl, bearerToken } = envConfig;
try { try {
if (!notification.id) { if (!notification.id) {
return; return;
@ -203,11 +225,11 @@ const deleteNotification = async (notification: Notification) => {
data: { isProcessing: false }, data: { isProcessing: false },
}); });
const response = await fetch( const response = await fetch(
`${process.env.PLEROMA_INSTANCE_URL}/api/v1/notifications/${notification.id}/dismiss`, `${pleromaInstanceUrl}/api/v1/notifications/${notification.id}/dismiss`,
{ {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${process.env.INSTANCE_BEARER_TOKEN}`, Authorization: `Bearer ${bearerToken}`,
}, },
} }
); );
@ -221,10 +243,6 @@ const deleteNotification = async (notification: Notification) => {
} }
}; };
const fetchInterval = process.env.FETCH_INTERVAL
? parseInt(process.env.FETCH_INTERVAL)
: 15000;
let notifications = []; let notifications = [];
const beginFetchCycle = async () => { const beginFetchCycle = async () => {
setInterval(async () => { setInterval(async () => {
@ -243,12 +261,12 @@ const beginFetchCycle = async () => {
}) })
); );
} }
}, 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 }, 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( console.log(
`Fetching notifications from ${process.env.PLEROMA_INSTANCE_DOMAIN}, every ${ `Fetching notifications from ${envConfig.pleromaInstanceDomain}, every ${
fetchInterval / 1000 envConfig.fetchInterval / 1000
} seconds.` } seconds.`
); );
await beginFetchCycle(); await beginFetchCycle();