diff --git a/.env.example b/.env.example index 336112b..f9bf0df 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ DATABASE_URL="file:../dev.db" # SQLite database relative to the ./prisma path PLEROMA_INSTANCE_URL="https://instance.tld" # Pleroma instance full URL including scheme PLEROMA_INSTANCE_DOMAIN="instance.tld" # used if you want to only want to respond to people from a particular instance PLEROMA_ACCOUNT_ID="" # obtained from /api/v1/accounts/{nickname} - used so we don't spam mentions when not directly addressed +REPLY_WITH_CONTEXT="" # set to true or false whether you want the bot to fetch context or not ONLY_WHITELIST="true" # change to "false" if you want to accept prompts from any and all domains - *** USE WITH CAUTION *** WHITELISTED_DOMAINS="" # comma separated list of domains you want to allow the bot to accept prompts from (i.e. poa.st,nicecrew.digital,detroitriotcity.com,decayable.ink) OLLAMA_URL="http://localhost:11434" # OLLAMA connection URL diff --git a/src/api.ts b/src/api.ts index 78359b3..ebcb729 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,5 @@ import { envConfig, prisma } from "./main.js"; -import { PleromaEmoji, Notification } from "../types.js"; +import { PleromaEmoji, Notification, ContextResponse } from "../types.js"; const getNotifications = async () => { const { bearerToken, pleromaInstanceUrl } = envConfig; @@ -22,6 +22,32 @@ const getNotifications = async () => { } }; +const getStatusContext = async (statusId: string) => { + const { bearerToken, pleromaInstanceUrl } = envConfig; + try { + const response = await fetch( + `${pleromaInstanceUrl}/api/v1/statuses/${statusId}/context`, + { + method: "GET", + headers: { + Authorization: `Bearer ${bearerToken}`, + }, + } + ); + if (!response.ok) { + throw new Error( + `Could not get conversation context: ${response.status} - ${response.statusText}` + ); + } + const data: ContextResponse = await response.json(); + return data; + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(error.message); + } + } +}; + const getInstanceEmojis = async () => { const { bearerToken, pleromaInstanceUrl } = envConfig; try { @@ -72,4 +98,9 @@ const deleteNotification = async (notification: Notification) => { } }; -export { deleteNotification, getInstanceEmojis, getNotifications }; +export { + deleteNotification, + getInstanceEmojis, + getNotifications, + getStatusContext, +}; diff --git a/src/main.ts b/src/main.ts index aa3ae0b..e150dee 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { // OllamaChatResponse, OllamaRequest, OllamaResponse, + PostAncestorsForModel, } from "../types.js"; // import striptags from "striptags"; import { PrismaClient } from "../generated/prisma/client.js"; @@ -13,13 +14,14 @@ import { getInstanceEmojis, deleteNotification, getNotifications, + getStatusContext, } from "./api.js"; import { storeUserData, storePromptData } from "./prisma.js"; import { isFromWhitelistedDomain, alreadyRespondedTo, recordPendingResponse, - trimInputData, + // trimInputData, selectRandomEmoji, shouldContinue, } from "./util.js"; @@ -44,6 +46,7 @@ export const envConfig = { ? parseInt(process.env.RANDOM_POST_INTERVAL) : 3600000, botAccountId: process.env.PLEROMA_ACCOUNT_ID, + replyWithContext: process.env.REPLY_WITH_CONTEXT === "true" ? true : false, }; const ollamaConfig: OllamaConfigOptions = { @@ -60,8 +63,13 @@ const ollamaConfig: OllamaConfigOptions = { const generateOllamaRequest = async ( notification: Notification ): Promise => { - const { whitelistOnly, ollamaModel, ollamaSystemPrompt, ollamaUrl } = - envConfig; + const { + whitelistOnly, + ollamaModel, + ollamaSystemPrompt, + ollamaUrl, + replyWithContext, + } = envConfig; try { if (shouldContinue(notification)) { if (whitelistOnly && !isFromWhitelistedDomain(notification)) { @@ -73,12 +81,30 @@ const generateOllamaRequest = async ( } 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); + } + const oneOffPrompt = `${notification.status.account.fqn} says: ${notification.status.pleroma.content["text/plain"]}\n[/INST]`; + const contextPrompt = `<>[INST]\n${ollamaSystemPrompt}\nHere is the previous conversation context in JSON format:\n${JSON.stringify( + conversationHistory + )}\nAssume the {account_fqn} key is the user who posted the {plaintext_content} to the users in {mentions}\nReply as if you are a party to the conversation. If you see '@nice-ai' or 'nice-ai' in the {mentions}, you are an addressee of the conversation.\nAppend the '@' sign to each username at the beginning when addressing users.<>`; const ollamaRequestBody: OllamaRequest = { model: ollamaModel, - prompt: `${notification.status.account.fqn} says: ${trimInputData( - notification.status.content - )}`, - system: ollamaSystemPrompt, + prompt: oneOffPrompt, + system: replyWithContext ? contextPrompt : ollamaSystemPrompt, stream: false, options: ollamaConfig, }; @@ -87,6 +113,7 @@ const generateOllamaRequest = async ( body: JSON.stringify(ollamaRequestBody), }); const ollamaResponse: OllamaResponse = await response.json(); + await storePromptData(notification, ollamaResponse); return ollamaResponse; } diff --git a/src/util.ts b/src/util.ts index 90078cb..921b8d0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -40,7 +40,7 @@ const shouldContinue = (notification: Notification) => { const { botAccountId } = envConfig; const statusContent = trimInputData(notification.status.content); if ( - notification.status.visibility !== "private" && + // notification.status.visibility !== "private" && !notification.account.bot && notification.type === "mention" ) { diff --git a/types.d.ts b/types.d.ts index 4ea03fb..ec24e1f 100644 --- a/types.d.ts +++ b/types.d.ts @@ -7,8 +7,38 @@ export interface Notification { } export interface ContextResponse { - ancestors: Notification[]; - descendents: Notification[]; + ancestors: ContextObject[]; + descendents: ContextObject[]; +} + +export interface PostAncestorsForModel { + account_fqn: string; + mentions: string[]; + plaintext_content: string; +} + +interface ContextAccountObject { + acct: string; + avatar: string; + bot: boolean; + display_name: string; + followers_count: number; + following_count: number; + fqn: string; + id: string; +} + +export interface ContextObject { + content: string; + id: string; + in_reply_to_account_id: string | null; + in_reply_to_id: string | null; + media_attachments: string[]; + mentions: Mention[]; + pleroma: PleromaObjectInResponse; + visibility: "public" | "private" | "unlisted"; + uri: string; + account: ContextAccountObject; } export interface NewStatusBody {