Compare commits
	
		
			3 Commits
		
	
	
		
			ee367a0d9a
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d40b51460e | |||
| 2a53b0a827 | |||
| 051a66ff26 | 
| @ -10,4 +10,5 @@ OLLAMA_SYSTEM_PROMPT="" # system prompt - used to help tune the responses from t | |||||||
| OLLAMA_MODEL="" # Ollama model for responses e.g dolphin-mistral:latest | 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 | 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 | 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) | 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, |   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) | ||||||
| @ -45,14 +41,18 @@ export const envConfig = { | |||||||
|     : 3600000, |     : 3600000, | ||||||
|   botAccountId: process.env.PLEROMA_ACCOUNT_ID, |   botAccountId: process.env.PLEROMA_ACCOUNT_ID, | ||||||
|   replyWithContext: process.env.REPLY_WITH_CONTEXT === "true" ? true : false, |   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 = { | const ollamaConfig: OllamaConfigOptions = { | ||||||
|   temperature: 0.9, |   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: 60, |   top_k: 40, | ||||||
|   num_ctx: 16384, // maximum context window for Llama 3.1 |   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 +68,128 @@ const generateOllamaRequest = async ( | |||||||
|     ollamaUrl, |     ollamaUrl, | ||||||
|     replyWithContext, |     replyWithContext, | ||||||
|   } = envConfig; |   } = envConfig; | ||||||
|  |  | ||||||
|  |   let shouldDeleteNotification = false; | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     if (shouldContinue(notification)) { |     if (!shouldContinue(notification)) { | ||||||
|       if (whitelistOnly && !isFromWhitelistedDomain(notification)) { |       shouldDeleteNotification = true; | ||||||
|         await deleteNotification(notification); |       return; | ||||||
|         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 (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) { |   } 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 |   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}`, |       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 +234,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 | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @ -193,15 +261,22 @@ const createTimelinePost = async () => { | |||||||
|     ollamaSystemPrompt, |     ollamaSystemPrompt, | ||||||
|     ollamaUrl, |     ollamaUrl, | ||||||
|     pleromaInstanceUrl, |     pleromaInstanceUrl, | ||||||
|  |     adHocPostInstructions, | ||||||
|   } = envConfig; |   } = envConfig; | ||||||
|   const ollamaRequestBody: OllamaChatRequest = { |   const ollamaRequestBody: OllamaChatRequest = { | ||||||
|     model: ollamaModel, |     model: ollamaModel, | ||||||
|     messages: [ |     messages: [ | ||||||
|       { role: "system", content: ollamaSystemPrompt as string }, |       { role: "system", content: ollamaSystemPrompt }, | ||||||
|       { role: "user", content: "Say something random." }, |       { | ||||||
|  |         role: "user", | ||||||
|  |         content: adHocPostInstructions, | ||||||
|  |       }, | ||||||
|     ], |     ], | ||||||
|     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 +319,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) { |  | ||||||
|             throw new Error(error.message); |  | ||||||
|           } |           } | ||||||
|         }) |         } 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 |   }, 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 |     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 +371,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(); | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user