Compare commits
6 Commits
a04cb9a6ad
...
ed3467b213
Author | SHA1 | Date | |
---|---|---|---|
ed3467b213 | |||
0f178fcfa9 | |||
0bfff52fd0 | |||
8e90e8b71e | |||
566d6ae518 | |||
2ec367f203 |
@ -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_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_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
|
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 ***
|
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)
|
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
|
OLLAMA_URL="http://localhost:11434" # OLLAMA connection URL
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pleroma-ollama-bot",
|
"name": "pleroma-ollama-bot",
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc && node -r dotenv/config dist/main.js",
|
"start": "tsc && node -r dotenv/config dist/main.js",
|
||||||
|
35
src/api.ts
35
src/api.ts
@ -1,5 +1,5 @@
|
|||||||
import { envConfig, prisma } from "./main.js";
|
import { envConfig, prisma } from "./main.js";
|
||||||
import { PleromaEmoji, Notification } from "../types.js";
|
import { PleromaEmoji, Notification, ContextResponse } from "../types.js";
|
||||||
|
|
||||||
const getNotifications = async () => {
|
const getNotifications = async () => {
|
||||||
const { bearerToken, pleromaInstanceUrl } = envConfig;
|
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 getInstanceEmojis = async () => {
|
||||||
const { bearerToken, pleromaInstanceUrl } = envConfig;
|
const { bearerToken, pleromaInstanceUrl } = envConfig;
|
||||||
try {
|
try {
|
||||||
@ -72,4 +98,9 @@ const deleteNotification = async (notification: Notification) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { deleteNotification, getInstanceEmojis, getNotifications };
|
export {
|
||||||
|
deleteNotification,
|
||||||
|
getInstanceEmojis,
|
||||||
|
getNotifications,
|
||||||
|
getStatusContext,
|
||||||
|
};
|
||||||
|
41
src/main.ts
41
src/main.ts
@ -6,6 +6,7 @@ import {
|
|||||||
// OllamaChatResponse,
|
// OllamaChatResponse,
|
||||||
OllamaRequest,
|
OllamaRequest,
|
||||||
OllamaResponse,
|
OllamaResponse,
|
||||||
|
PostAncestorsForModel,
|
||||||
} from "../types.js";
|
} from "../types.js";
|
||||||
// import striptags from "striptags";
|
// import striptags from "striptags";
|
||||||
import { PrismaClient } from "../generated/prisma/client.js";
|
import { PrismaClient } from "../generated/prisma/client.js";
|
||||||
@ -13,13 +14,14 @@ import {
|
|||||||
getInstanceEmojis,
|
getInstanceEmojis,
|
||||||
deleteNotification,
|
deleteNotification,
|
||||||
getNotifications,
|
getNotifications,
|
||||||
|
getStatusContext,
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
import { storeUserData, storePromptData } from "./prisma.js";
|
import { storeUserData, storePromptData } from "./prisma.js";
|
||||||
import {
|
import {
|
||||||
isFromWhitelistedDomain,
|
isFromWhitelistedDomain,
|
||||||
alreadyRespondedTo,
|
alreadyRespondedTo,
|
||||||
recordPendingResponse,
|
recordPendingResponse,
|
||||||
trimInputData,
|
// trimInputData,
|
||||||
selectRandomEmoji,
|
selectRandomEmoji,
|
||||||
shouldContinue,
|
shouldContinue,
|
||||||
} from "./util.js";
|
} from "./util.js";
|
||||||
@ -44,6 +46,7 @@ export const envConfig = {
|
|||||||
? parseInt(process.env.RANDOM_POST_INTERVAL)
|
? parseInt(process.env.RANDOM_POST_INTERVAL)
|
||||||
: 3600000,
|
: 3600000,
|
||||||
botAccountId: process.env.PLEROMA_ACCOUNT_ID,
|
botAccountId: process.env.PLEROMA_ACCOUNT_ID,
|
||||||
|
replyWithContext: process.env.REPLY_WITH_CONTEXT === "true" ? true : false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ollamaConfig: OllamaConfigOptions = {
|
const ollamaConfig: OllamaConfigOptions = {
|
||||||
@ -60,8 +63,13 @@ const ollamaConfig: OllamaConfigOptions = {
|
|||||||
const generateOllamaRequest = async (
|
const generateOllamaRequest = async (
|
||||||
notification: Notification
|
notification: Notification
|
||||||
): Promise<OllamaResponse | undefined> => {
|
): Promise<OllamaResponse | undefined> => {
|
||||||
const { whitelistOnly, ollamaModel, ollamaSystemPrompt, ollamaUrl } =
|
const {
|
||||||
envConfig;
|
whitelistOnly,
|
||||||
|
ollamaModel,
|
||||||
|
ollamaSystemPrompt,
|
||||||
|
ollamaUrl,
|
||||||
|
replyWithContext,
|
||||||
|
} = envConfig;
|
||||||
try {
|
try {
|
||||||
if (shouldContinue(notification)) {
|
if (shouldContinue(notification)) {
|
||||||
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
if (whitelistOnly && !isFromWhitelistedDomain(notification)) {
|
||||||
@ -73,12 +81,30 @@ const generateOllamaRequest = async (
|
|||||||
}
|
}
|
||||||
await recordPendingResponse(notification);
|
await recordPendingResponse(notification);
|
||||||
await storeUserData(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 = `<<SYS>>[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.<</SYS>>`;
|
||||||
const ollamaRequestBody: OllamaRequest = {
|
const ollamaRequestBody: OllamaRequest = {
|
||||||
model: ollamaModel,
|
model: ollamaModel,
|
||||||
prompt: `${notification.status.account.fqn} says: ${trimInputData(
|
prompt: oneOffPrompt,
|
||||||
notification.status.content
|
system: replyWithContext ? contextPrompt : ollamaSystemPrompt,
|
||||||
)}`,
|
|
||||||
system: ollamaSystemPrompt,
|
|
||||||
stream: false,
|
stream: false,
|
||||||
options: ollamaConfig,
|
options: ollamaConfig,
|
||||||
};
|
};
|
||||||
@ -87,6 +113,7 @@ const generateOllamaRequest = async (
|
|||||||
body: JSON.stringify(ollamaRequestBody),
|
body: JSON.stringify(ollamaRequestBody),
|
||||||
});
|
});
|
||||||
const ollamaResponse: OllamaResponse = await response.json();
|
const ollamaResponse: OllamaResponse = await response.json();
|
||||||
|
|
||||||
await storePromptData(notification, ollamaResponse);
|
await storePromptData(notification, ollamaResponse);
|
||||||
return ollamaResponse;
|
return ollamaResponse;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ const shouldContinue = (notification: Notification) => {
|
|||||||
const { botAccountId } = envConfig;
|
const { botAccountId } = envConfig;
|
||||||
const statusContent = trimInputData(notification.status.content);
|
const statusContent = trimInputData(notification.status.content);
|
||||||
if (
|
if (
|
||||||
notification.status.visibility !== "private" &&
|
// notification.status.visibility !== "private" &&
|
||||||
!notification.account.bot &&
|
!notification.account.bot &&
|
||||||
notification.type === "mention"
|
notification.type === "mention"
|
||||||
) {
|
) {
|
||||||
|
@ -5,7 +5,7 @@ After=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=USERNAME_HERE
|
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=/usr/bin/screen -L -DmS pleroma-ollama-bot /home/bot/.nvm/versions/node/v22.11.0/bin/npm run start
|
||||||
|
45
types.d.ts
vendored
45
types.d.ts
vendored
@ -6,6 +6,41 @@ export interface Notification {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContextResponse {
|
||||||
|
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 {
|
export interface NewStatusBody {
|
||||||
content_type: "application/json" | "text/markdown";
|
content_type: "application/json" | "text/markdown";
|
||||||
in_reply_to_id?: string;
|
in_reply_to_id?: string;
|
||||||
@ -94,9 +129,19 @@ export interface Status {
|
|||||||
in_reply_to_account_id: string; // account ID of the reply
|
in_reply_to_account_id: string; // account ID of the reply
|
||||||
in_reply_to_id: string; // status that the user has replied to
|
in_reply_to_id: string; // status that the user has replied to
|
||||||
mentions: Mention[]; // array of mentions
|
mentions: Mention[]; // array of mentions
|
||||||
|
pleroma: PleromaObjectInResponse;
|
||||||
visibility: "private" | "public" | "unlisted";
|
visibility: "private" | "public" | "unlisted";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PleromaObjectInResponse {
|
||||||
|
content: { "text/plain": string };
|
||||||
|
context: string;
|
||||||
|
conversation_id: number;
|
||||||
|
direct_conversation_id: number | null;
|
||||||
|
local: boolean;
|
||||||
|
in_reply_to_account_acct: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Mention {
|
export interface Mention {
|
||||||
acct: string;
|
acct: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
Reference in New Issue
Block a user