Compare commits
3 Commits
ee367a0d9a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d40b51460e | |||
| 2a53b0a827 | |||
| 051a66ff26 |
@ -11,3 +11,4 @@ OLLAMA_MODEL="" # Ollama model for responses e.g dolphin-mistral:latest
|
||||
FETCH_INTERVAL="" # interval for fetching new notifications from the instance, in milliseconds, recommend at least 15000
|
||||
RANDOM_POST_INTERVAL="" # interval for ad-hoc posts in milliseconds
|
||||
INSTANCE_BEARER_TOKEN="" # instance auth/bearer token (check the "verify_credentials" endpoint request headers in Chrome DevTools if on Soapbox)
|
||||
AD_HOC_POST_INSTRUCTIONS=""
|
||||
304
src/main.ts
304
src/main.ts
@ -4,12 +4,10 @@ import {
|
||||
OllamaConfigOptions,
|
||||
OllamaChatRequest,
|
||||
OllamaChatResponse,
|
||||
PostAncestorsForModel,
|
||||
// PostAncestorsForModel,
|
||||
} from "../types.js";
|
||||
// import striptags from "striptags";
|
||||
import { PrismaClient } from "../generated/prisma/client.js";
|
||||
import {
|
||||
// getInstanceEmojis,
|
||||
deleteNotification,
|
||||
getNotifications,
|
||||
getStatusContext,
|
||||
@ -19,8 +17,6 @@ import {
|
||||
isFromWhitelistedDomain,
|
||||
alreadyRespondedTo,
|
||||
recordPendingResponse,
|
||||
// trimInputData,
|
||||
// selectRandomEmoji,
|
||||
shouldContinue,
|
||||
} from "./util.js";
|
||||
|
||||
@ -34,7 +30,7 @@ export const envConfig = {
|
||||
? process.env.WHITELISTED_DOMAINS.split(",")
|
||||
: [process.env.PLEROMA_INSTANCE_DOMAIN],
|
||||
ollamaUrl: process.env.OLLAMA_URL || "",
|
||||
ollamaSystemPrompt: process.env.OLLAMA_SYSTEM_PROMPT,
|
||||
ollamaSystemPrompt: process.env.OLLAMA_SYSTEM_PROMPT || "",
|
||||
ollamaModel: process.env.OLLAMA_MODEL || "",
|
||||
fetchInterval: process.env.FETCH_INTERVAL
|
||||
? parseInt(process.env.FETCH_INTERVAL)
|
||||
@ -45,14 +41,18 @@ export const envConfig = {
|
||||
: 3600000,
|
||||
botAccountId: process.env.PLEROMA_ACCOUNT_ID,
|
||||
replyWithContext: process.env.REPLY_WITH_CONTEXT === "true" ? true : false,
|
||||
adHocPostInstructions: process.env.AD_HOC_POST_INSTRUCTIONS
|
||||
? process.env.AD_HOC_POST_INSTRUCTIONS
|
||||
: "Say something.",
|
||||
};
|
||||
|
||||
const ollamaConfig: OllamaConfigOptions = {
|
||||
temperature: 0.9,
|
||||
top_p: 0.85,
|
||||
top_k: 60,
|
||||
num_ctx: 16384, // maximum context window for Llama 3.1
|
||||
repeat_penalty: 1.1,
|
||||
temperature: 0.85, // Increased from 0.6 - more creative and varied
|
||||
top_p: 0.9, // Slightly increased for more diverse responses
|
||||
top_k: 40,
|
||||
num_ctx: 16384,
|
||||
repeat_penalty: 1.1, // Reduced from 1.15 - less mechanical
|
||||
// stop: ['<|im_end|>', '\n\n']
|
||||
};
|
||||
|
||||
// this could be helpful
|
||||
@ -68,75 +68,128 @@ const generateOllamaRequest = async (
|
||||
ollamaUrl,
|
||||
replyWithContext,
|
||||
} = envConfig;
|
||||
|
||||
let shouldDeleteNotification = false;
|
||||
|
||||
try {
|
||||
if (shouldContinue(notification)) {
|
||||
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
||||
await deleteNotification(notification);
|
||||
return;
|
||||
}
|
||||
if (await alreadyRespondedTo(notification)) {
|
||||
return;
|
||||
}
|
||||
await recordPendingResponse(notification);
|
||||
await storeUserData(notification);
|
||||
let conversationHistory: PostAncestorsForModel[] = [];
|
||||
if (replyWithContext) {
|
||||
const contextPosts = await getStatusContext(notification.status.id);
|
||||
if (!contextPosts?.ancestors || !contextPosts) {
|
||||
throw new Error(`Unable to obtain post context ancestors.`);
|
||||
}
|
||||
conversationHistory = contextPosts.ancestors.map((ancestor) => {
|
||||
const mentions = ancestor.mentions.map((mention) => mention.acct);
|
||||
return {
|
||||
account_fqn: ancestor.account.fqn,
|
||||
mentions,
|
||||
plaintext_content: ancestor.pleroma.content["text/plain"],
|
||||
};
|
||||
});
|
||||
// console.log(conversationHistory);
|
||||
}
|
||||
|
||||
// Simplified user message (remove [/INST] as it's not needed for Llama 3)
|
||||
const userMessage = `${notification.status.account.fqn} says to you: \"${notification.status.pleroma.content["text/plain"]}\".`;
|
||||
|
||||
let systemContent = ollamaSystemPrompt;
|
||||
if (replyWithContext) {
|
||||
// Simplified context instructions (avoid heavy JSON; summarize for clarity)
|
||||
systemContent = `${ollamaSystemPrompt}\n\nPrevious conversation context:\n${conversationHistory
|
||||
.map(
|
||||
(post) =>
|
||||
`${post.account_fqn} (said to ${post.mentions.join(", ")}): ${
|
||||
post.plaintext_content
|
||||
}`
|
||||
)
|
||||
.join(
|
||||
"\n"
|
||||
)}\nReply to the user who addressed you (you are Lexi, also known as nice-ai or nice-ai@nicecrew.digital). Examine the context of the entire conversation and make references to topics or information where appropriate. Prefix usernames with '@' when addressing them. Assume if there is no domain in the username, the domain is @nicecrew.digital (for example @matty would be @matty@nicecrew.digital)`;
|
||||
}
|
||||
|
||||
// Switch to chat request format (messages array auto-handles Llama 3 template)
|
||||
const ollamaRequestBody: OllamaChatRequest = {
|
||||
model: ollamaModel,
|
||||
messages: [
|
||||
{ role: "system", content: systemContent as string },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
stream: false,
|
||||
options: ollamaConfig,
|
||||
};
|
||||
|
||||
// Change endpoint to /api/chat
|
||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(ollamaRequestBody),
|
||||
});
|
||||
const ollamaResponse: OllamaChatResponse = await response.json();
|
||||
|
||||
await storePromptData(notification, ollamaResponse);
|
||||
return ollamaResponse;
|
||||
if (!shouldContinue(notification)) {
|
||||
shouldDeleteNotification = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
||||
shouldDeleteNotification = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (await alreadyRespondedTo(notification)) {
|
||||
shouldDeleteNotification = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await recordPendingResponse(notification);
|
||||
await storeUserData(notification);
|
||||
|
||||
let conversationContext = "";
|
||||
if (replyWithContext) {
|
||||
const contextPosts = await getStatusContext(notification.status.id);
|
||||
if (!contextPosts?.ancestors) {
|
||||
throw new Error(`Unable to obtain post context ancestors.`);
|
||||
}
|
||||
|
||||
// Build a human-readable conversation thread
|
||||
const allPosts = [...contextPosts.ancestors];
|
||||
|
||||
// Include descendants (follow-up posts) if available
|
||||
if (contextPosts.descendents && contextPosts.descendents.length > 0) {
|
||||
allPosts.push(...contextPosts.descendents);
|
||||
}
|
||||
|
||||
if (allPosts.length > 0) {
|
||||
const conversationLines = allPosts.map((post) => {
|
||||
const author = post.account.fqn;
|
||||
const content = post.pleroma.content["text/plain"];
|
||||
const replyingTo = post.in_reply_to_account_id
|
||||
? ` (replying to another message)`
|
||||
: "";
|
||||
return `[@${author}${replyingTo}]: ${content}`;
|
||||
});
|
||||
|
||||
conversationContext = `
|
||||
Previous conversation thread:
|
||||
${conversationLines.join("\n\n")}
|
||||
---
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const userMessage = notification.status.pleroma.content["text/plain"];
|
||||
const originalAuthor = notification.account.fqn;
|
||||
|
||||
let systemContent = ollamaSystemPrompt;
|
||||
if (replyWithContext && conversationContext) {
|
||||
systemContent = `${ollamaSystemPrompt}
|
||||
|
||||
${conversationContext}
|
||||
Current message from @${originalAuthor}:
|
||||
"${userMessage}"
|
||||
|
||||
Instructions:
|
||||
- You are replying to @${originalAuthor}
|
||||
- Address them directly if appropriate
|
||||
- Use markdown formatting and emojis sparingly`;
|
||||
}
|
||||
|
||||
const ollamaRequestBody: OllamaChatRequest = {
|
||||
model: ollamaModel,
|
||||
messages: [
|
||||
{ role: "system", content: systemContent },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
stream: false,
|
||||
options: {
|
||||
...ollamaConfig,
|
||||
stop: ["</s>", "[INST]"], // Mistral 0.3 stop tokens
|
||||
},
|
||||
};
|
||||
|
||||
console.log(
|
||||
`Generating response for notification ${notification.id} from @${originalAuthor}`
|
||||
);
|
||||
|
||||
// Change endpoint to /api/chat
|
||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(ollamaRequestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API request failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const ollamaResponse: OllamaChatResponse = await response.json();
|
||||
|
||||
await storePromptData(notification, ollamaResponse);
|
||||
return ollamaResponse;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message);
|
||||
console.error(
|
||||
`Error in generateOllamaRequest for notification ${notification.id}:`,
|
||||
error.message
|
||||
);
|
||||
// Delete notification on error to prevent retry loops
|
||||
shouldDeleteNotification = true;
|
||||
throw error;
|
||||
} finally {
|
||||
if (shouldDeleteNotification) {
|
||||
try {
|
||||
await deleteNotification(notification);
|
||||
} catch (deleteError: any) {
|
||||
console.error(
|
||||
`Failed to delete notification ${notification.id}:`,
|
||||
deleteError.message
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -145,27 +198,28 @@ const postReplyToStatus = async (
|
||||
ollamaResponseBody: OllamaChatResponse
|
||||
) => {
|
||||
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
||||
// const emojiList = await getInstanceEmojis();
|
||||
// let randomEmoji;
|
||||
// if (emojiList) {
|
||||
// randomEmoji = selectRandomEmoji(emojiList);
|
||||
// }
|
||||
|
||||
try {
|
||||
let mentions: string[];
|
||||
// Only mention the original author who triggered the bot
|
||||
const originalAuthor = notification.account.acct;
|
||||
console.log(
|
||||
`Replying to: @${originalAuthor} (status ID: ${notification.status.id})`
|
||||
);
|
||||
|
||||
// Sanitize LLM output - remove any stray Mistral special tokens
|
||||
let sanitizedContent = ollamaResponseBody.message.content
|
||||
.replace(/<\/s>/g, "") // Remove EOS token if it appears
|
||||
.replace(/\[INST\]/g, "") // Remove instruction start token
|
||||
.replace(/\[\/INST\]/g, "") // Remove instruction end token
|
||||
.replace(/<s>/g, "") // Remove BOS token if it appears
|
||||
.trim();
|
||||
|
||||
const statusBody: NewStatusBody = {
|
||||
content_type: "text/markdown",
|
||||
status: `${ollamaResponseBody.message.content}`,
|
||||
status: sanitizedContent,
|
||||
in_reply_to_id: notification.status.id,
|
||||
to: [originalAuthor], // Only send to the person who mentioned the bot
|
||||
};
|
||||
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",
|
||||
@ -180,9 +234,23 @@ const postReplyToStatus = async (
|
||||
throw new Error(`New status request failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
await deleteNotification(notification);
|
||||
console.log(`Successfully posted reply to @${originalAuthor}`);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message);
|
||||
console.error(
|
||||
`Error posting reply for notification ${notification.id}:`,
|
||||
error.message
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
// Always try to delete the notification, even if posting failed
|
||||
try {
|
||||
await deleteNotification(notification);
|
||||
} catch (deleteError: any) {
|
||||
console.error(
|
||||
`Failed to delete notification ${notification.id}:`,
|
||||
deleteError.message
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -193,15 +261,22 @@ const createTimelinePost = async () => {
|
||||
ollamaSystemPrompt,
|
||||
ollamaUrl,
|
||||
pleromaInstanceUrl,
|
||||
adHocPostInstructions,
|
||||
} = envConfig;
|
||||
const ollamaRequestBody: OllamaChatRequest = {
|
||||
model: ollamaModel,
|
||||
messages: [
|
||||
{ role: "system", content: ollamaSystemPrompt as string },
|
||||
{ role: "user", content: "Say something random." },
|
||||
{ role: "system", content: ollamaSystemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: adHocPostInstructions,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
options: ollamaConfig,
|
||||
options: {
|
||||
...ollamaConfig,
|
||||
stop: ["</s>", "[INST]"], // Mistral 0.3 stop tokens
|
||||
},
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||
@ -244,18 +319,21 @@ 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);
|
||||
// Process notifications sequentially to avoid race conditions
|
||||
for (const notification of notifications) {
|
||||
try {
|
||||
const ollamaResponse = await generateOllamaRequest(notification);
|
||||
if (ollamaResponse) {
|
||||
await postReplyToStatus(notification, ollamaResponse);
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`Error processing notification ${notification.id}:`,
|
||||
error.message
|
||||
);
|
||||
// Continue processing other notifications even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 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
|
||||
};
|
||||
@ -277,6 +355,11 @@ console.log(
|
||||
envConfig.fetchInterval / 1000
|
||||
} seconds.`
|
||||
);
|
||||
console.log(
|
||||
`Making ad-hoc post to ${envConfig.pleromaInstanceDomain}, every ${
|
||||
envConfig.adHocPostInterval / 1000 / 60
|
||||
} minutes.`
|
||||
);
|
||||
console.log(
|
||||
`Accepting prompts from: ${envConfig.whitelistedDomains.join(", ")}`
|
||||
);
|
||||
@ -288,7 +371,4 @@ console.log(
|
||||
console.log(`System prompt: ${envConfig.ollamaSystemPrompt}`);
|
||||
|
||||
await beginFetchCycle();
|
||||
// setInterval(async () => {
|
||||
// createTimelinePost();
|
||||
// }, 10000);
|
||||
await beginStatusPostInterval();
|
||||
|
||||
Reference in New Issue
Block a user