Compare commits
6 Commits
tyler
...
2a53b0a827
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a53b0a827 | |||
| 051a66ff26 | |||
| ee367a0d9a | |||
| e696343a73 | |||
| 88a0710c55 | |||
| 75fa4cea8b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
# Keep environment variables out of version control
|
# Keep environment variables out of version control
|
||||||
.env
|
.env*
|
||||||
*.log
|
*.log
|
||||||
*.db
|
*.db
|
||||||
/dist
|
/dist
|
||||||
|
|||||||
219
src/main.ts
219
src/main.ts
@ -4,12 +4,10 @@ import {
|
|||||||
OllamaConfigOptions,
|
OllamaConfigOptions,
|
||||||
OllamaChatRequest,
|
OllamaChatRequest,
|
||||||
OllamaChatResponse,
|
OllamaChatResponse,
|
||||||
PostAncestorsForModel,
|
// PostAncestorsForModel,
|
||||||
} from "../types.js";
|
} from "../types.js";
|
||||||
// import striptags from "striptags";
|
|
||||||
import { PrismaClient } from "../generated/prisma/client.js";
|
import { PrismaClient } from "../generated/prisma/client.js";
|
||||||
import {
|
import {
|
||||||
getInstanceEmojis,
|
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
getNotifications,
|
getNotifications,
|
||||||
getStatusContext,
|
getStatusContext,
|
||||||
@ -19,8 +17,6 @@ import {
|
|||||||
isFromWhitelistedDomain,
|
isFromWhitelistedDomain,
|
||||||
alreadyRespondedTo,
|
alreadyRespondedTo,
|
||||||
recordPendingResponse,
|
recordPendingResponse,
|
||||||
// trimInputData,
|
|
||||||
selectRandomEmoji,
|
|
||||||
shouldContinue,
|
shouldContinue,
|
||||||
} from "./util.js";
|
} from "./util.js";
|
||||||
|
|
||||||
@ -34,7 +30,7 @@ export const envConfig = {
|
|||||||
? process.env.WHITELISTED_DOMAINS.split(",")
|
? process.env.WHITELISTED_DOMAINS.split(",")
|
||||||
: [process.env.PLEROMA_INSTANCE_DOMAIN],
|
: [process.env.PLEROMA_INSTANCE_DOMAIN],
|
||||||
ollamaUrl: process.env.OLLAMA_URL || "",
|
ollamaUrl: process.env.OLLAMA_URL || "",
|
||||||
ollamaSystemPrompt: process.env.OLLAMA_SYSTEM_PROMPT,
|
ollamaSystemPrompt: process.env.OLLAMA_SYSTEM_PROMPT || "",
|
||||||
ollamaModel: process.env.OLLAMA_MODEL || "",
|
ollamaModel: process.env.OLLAMA_MODEL || "",
|
||||||
fetchInterval: process.env.FETCH_INTERVAL
|
fetchInterval: process.env.FETCH_INTERVAL
|
||||||
? parseInt(process.env.FETCH_INTERVAL)
|
? parseInt(process.env.FETCH_INTERVAL)
|
||||||
@ -48,11 +44,12 @@ export const envConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ollamaConfig: OllamaConfigOptions = {
|
const ollamaConfig: OllamaConfigOptions = {
|
||||||
temperature: 0.6,
|
temperature: 0.85, // Increased from 0.6 - more creative and varied
|
||||||
top_p: 0.85,
|
top_p: 0.9, // Slightly increased for more diverse responses
|
||||||
top_k: 40,
|
top_k: 40,
|
||||||
num_ctx: 8192,
|
num_ctx: 16384,
|
||||||
repeat_penalty: 1.1,
|
repeat_penalty: 1.1, // Reduced from 1.15 - less mechanical
|
||||||
|
// stop: ['<|im_end|>', '\n\n']
|
||||||
};
|
};
|
||||||
|
|
||||||
// this could be helpful
|
// this could be helpful
|
||||||
@ -68,75 +65,128 @@ const generateOllamaRequest = async (
|
|||||||
ollamaUrl,
|
ollamaUrl,
|
||||||
replyWithContext,
|
replyWithContext,
|
||||||
} = envConfig;
|
} = envConfig;
|
||||||
|
|
||||||
|
let shouldDeleteNotification = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (shouldContinue(notification)) {
|
if (!shouldContinue(notification)) {
|
||||||
|
shouldDeleteNotification = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
||||||
await deleteNotification(notification);
|
shouldDeleteNotification = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await alreadyRespondedTo(notification)) {
|
if (await alreadyRespondedTo(notification)) {
|
||||||
|
shouldDeleteNotification = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await recordPendingResponse(notification);
|
await recordPendingResponse(notification);
|
||||||
await storeUserData(notification);
|
await storeUserData(notification);
|
||||||
let conversationHistory: PostAncestorsForModel[] = [];
|
|
||||||
|
let conversationContext = "";
|
||||||
if (replyWithContext) {
|
if (replyWithContext) {
|
||||||
const contextPosts = await getStatusContext(notification.status.id);
|
const contextPosts = await getStatusContext(notification.status.id);
|
||||||
if (!contextPosts?.ancestors || !contextPosts) {
|
if (!contextPosts?.ancestors) {
|
||||||
throw new Error(`Unable to obtain post context ancestors.`);
|
throw new Error(`Unable to obtain post context ancestors.`);
|
||||||
}
|
}
|
||||||
conversationHistory = contextPosts.ancestors.map((ancestor) => {
|
|
||||||
const mentions = ancestor.mentions.map((mention) => mention.acct);
|
// Build a human-readable conversation thread
|
||||||
return {
|
const allPosts = [...contextPosts.ancestors];
|
||||||
account_fqn: ancestor.account.fqn,
|
|
||||||
mentions,
|
// Include descendants (follow-up posts) if available
|
||||||
plaintext_content: ancestor.pleroma.content["text/plain"],
|
if (contextPosts.descendents && contextPosts.descendents.length > 0) {
|
||||||
};
|
allPosts.push(...contextPosts.descendents);
|
||||||
});
|
|
||||||
// console.log(conversationHistory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified user message (remove [/INST] as it's not needed for Llama 3)
|
if (allPosts.length > 0) {
|
||||||
const userMessage = `${notification.status.account.fqn} says: ${notification.status.pleroma.content["text/plain"]}`;
|
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;
|
let systemContent = ollamaSystemPrompt;
|
||||||
if (replyWithContext) {
|
if (replyWithContext && conversationContext) {
|
||||||
// Simplified context instructions (avoid heavy JSON; summarize for clarity)
|
systemContent = `${ollamaSystemPrompt}
|
||||||
systemContent = `${ollamaSystemPrompt}\n\nPrevious conversation context:\n${conversationHistory
|
|
||||||
.map(
|
${conversationContext}
|
||||||
(post) =>
|
Current message from @${originalAuthor}:
|
||||||
`${post.account_fqn} (to ${post.mentions.join(", ")}): ${
|
"${userMessage}"
|
||||||
post.plaintext_content
|
|
||||||
}`
|
Instructions:
|
||||||
)
|
- You are replying to @${originalAuthor}
|
||||||
.join(
|
- Address them directly if appropriate
|
||||||
"\n"
|
- Use markdown formatting and emojis sparingly`;
|
||||||
)}\nReply as if you are a party to the conversation. If '@nice-ai' is mentioned, respond directly. Prefix usernames with '@' when addressing them.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to chat request format (messages array auto-handles Llama 3 template)
|
|
||||||
const ollamaRequestBody: OllamaChatRequest = {
|
const ollamaRequestBody: OllamaChatRequest = {
|
||||||
model: ollamaModel,
|
model: ollamaModel,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: systemContent as string },
|
{ role: "system", content: systemContent },
|
||||||
{ role: "user", content: userMessage },
|
{ role: "user", content: userMessage },
|
||||||
],
|
],
|
||||||
stream: false,
|
stream: false,
|
||||||
options: ollamaConfig,
|
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
|
// Change endpoint to /api/chat
|
||||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(ollamaRequestBody),
|
body: JSON.stringify(ollamaRequestBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ollama API request failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
const ollamaResponse: OllamaChatResponse = await response.json();
|
const ollamaResponse: OllamaChatResponse = await response.json();
|
||||||
|
|
||||||
await storePromptData(notification, ollamaResponse);
|
await storePromptData(notification, ollamaResponse);
|
||||||
return ollamaResponse;
|
return ollamaResponse;
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} 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 +195,28 @@ const postReplyToStatus = async (
|
|||||||
ollamaResponseBody: OllamaChatResponse
|
ollamaResponseBody: OllamaChatResponse
|
||||||
) => {
|
) => {
|
||||||
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
const { pleromaInstanceUrl, bearerToken } = envConfig;
|
||||||
const emojiList = await getInstanceEmojis();
|
|
||||||
let randomEmoji;
|
|
||||||
if (emojiList) {
|
|
||||||
randomEmoji = selectRandomEmoji(emojiList);
|
|
||||||
}
|
|
||||||
try {
|
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 = {
|
const statusBody: NewStatusBody = {
|
||||||
content_type: "text/markdown",
|
content_type: "text/markdown",
|
||||||
status: `${ollamaResponseBody.message.content} :${randomEmoji}:`,
|
status: sanitizedContent,
|
||||||
in_reply_to_id: notification.status.id,
|
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`, {
|
const response = await fetch(`${pleromaInstanceUrl}/api/v1/statuses`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -180,9 +231,23 @@ const postReplyToStatus = async (
|
|||||||
throw new Error(`New status request failed: ${response.statusText}`);
|
throw new Error(`New status request failed: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteNotification(notification);
|
console.log(`Successfully posted reply to @${originalAuthor}`);
|
||||||
} catch (error: any) {
|
} 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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -198,10 +263,17 @@ const createTimelinePost = async () => {
|
|||||||
model: ollamaModel,
|
model: ollamaModel,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "system", content: ollamaSystemPrompt as string },
|
{ role: "system", content: ollamaSystemPrompt as string },
|
||||||
{ role: "user", content: "Say something random." },
|
{
|
||||||
|
role: "user",
|
||||||
|
content:
|
||||||
|
"Make a post about something. Keep your tone authentic, as if you are a real person making a post about a topic that interests you on a microblogging platform. This can be about anything like politics, gardening, homesteading, your favorite animal, a fun fact, what happened during your day, seeking companionship, baking, cooking, et cetera. Do not format the post with a title or quotes, nor sign the post with your name. It will be posted to your timeline so everyone will know you said it.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
stream: false,
|
stream: false,
|
||||||
options: ollamaConfig,
|
options: {
|
||||||
|
...ollamaConfig,
|
||||||
|
stop: ["</s>", "[INST]"], // Mistral 0.3 stop tokens
|
||||||
|
},
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
const response = await fetch(`${ollamaUrl}/api/chat`, {
|
||||||
@ -244,18 +316,21 @@ const beginFetchCycle = async () => {
|
|||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
notifications = await getNotifications();
|
notifications = await getNotifications();
|
||||||
if (notifications.length > 0) {
|
if (notifications.length > 0) {
|
||||||
await Promise.all(
|
// Process notifications sequentially to avoid race conditions
|
||||||
notifications.map(async (notification) => {
|
for (const notification of notifications) {
|
||||||
try {
|
try {
|
||||||
const ollamaResponse = await generateOllamaRequest(notification);
|
const ollamaResponse = await generateOllamaRequest(notification);
|
||||||
if (ollamaResponse) {
|
if (ollamaResponse) {
|
||||||
postReplyToStatus(notification, ollamaResponse);
|
await postReplyToStatus(notification, ollamaResponse);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.message);
|
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
|
}, 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 +352,11 @@ console.log(
|
|||||||
envConfig.fetchInterval / 1000
|
envConfig.fetchInterval / 1000
|
||||||
} seconds.`
|
} seconds.`
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
`Making ad-hoc post to ${envConfig.pleromaInstanceDomain}, every ${
|
||||||
|
envConfig.adHocPostInterval / 1000 / 60
|
||||||
|
} minutes.`
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
`Accepting prompts from: ${envConfig.whitelistedDomains.join(", ")}`
|
`Accepting prompts from: ${envConfig.whitelistedDomains.join(", ")}`
|
||||||
);
|
);
|
||||||
@ -288,7 +368,4 @@ console.log(
|
|||||||
console.log(`System prompt: ${envConfig.ollamaSystemPrompt}`);
|
console.log(`System prompt: ${envConfig.ollamaSystemPrompt}`);
|
||||||
|
|
||||||
await beginFetchCycle();
|
await beginFetchCycle();
|
||||||
// setInterval(async () => {
|
|
||||||
// createTimelinePost();
|
|
||||||
// }, 10000);
|
|
||||||
await beginStatusPostInterval();
|
await beginStatusPostInterval();
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Type=simple
|
|||||||
User=bot
|
User=bot
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
ExecStart=/usr/bin/screen -L -DmS pleroma-ollama-bot /home/bot/.nvm/versions/node/v22.11.0/bin/npm run start
|
ExecStart=/home/bot/.nvm/versions/node/v22.11.0/bin/npm run start
|
||||||
WorkingDirectory=/path/to/directory
|
WorkingDirectory=/path/to/directory
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user