Skip to content

WIP: tasks #4010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dev-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ concurrency:

on:
push:
branches: ['3982-disable-login-simple-sso'] # put your current branch to create a build. Core team only.
branches: ['tasks'] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'
Expand Down
25 changes: 25 additions & 0 deletions frontend/public/service-workers/push.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function parseEventData(event) {
try {
return event.data.json();
} catch (e) {
console.error('Failed to parse event data - is payload valid? .text():\n', event.data.text());
return null
}
}

self.addEventListener('push', function (event) {
const data = parseEventData(event);
if (!data) return;
self.registration.showNotification(data.title || 'AnythingLLM', {
body: data.message,
icon: '/favicon.png',
data: { ...data }
});
});

self.addEventListener('notificationclick', function (event) {
event.notification.close();
const { onClickUrl = null } = event.notification.data || {};
if (!onClickUrl) return;
event.waitUntil(clients.openWindow(onClickUrl));
});
42 changes: 41 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { lazy, Suspense } from "react";
import React, { lazy, Suspense, useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { ContextWrapper } from "@/AuthContext";
Expand All @@ -18,6 +18,7 @@ import { LogoProvider } from "./LogoContext";
import { FullScreenLoader } from "./components/Preloader";
import { ThemeProvider } from "./ThemeContext";
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
import { API_BASE } from "./utils/constants";

const Main = lazy(() => import("@/pages/Main"));
const InvitePage = lazy(() => import("@/pages/Invite"));
Expand Down Expand Up @@ -90,7 +91,46 @@ const SystemPromptVariables = lazy(
() => import("@/pages/Admin/SystemPromptVariables")
);

function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
return new Uint8Array([...rawData].map((char) => char.charCodeAt(0)));
}

function usePushNotifications() {
useEffect(() => {

async function setupPushNotifications() {
const { publicKey } = await fetch(`${API_BASE}/push-public-key`).then(res => res.json());
console.log('WPPK', publicKey);

if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/service-workers/push.js').then(swReg => {
swReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
}).then(subscription => {
fetch(`${API_BASE}/subscribe`, {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
});
});
} else {
console.log('Push notifications are not supported in this browser');
}
}
setupPushNotifications();
}, []);
}

export default function App() {
usePushNotifications();

return (
<ThemeProvider>
<Suspense fallback={<FullScreenLoader />}>
Expand Down
1 change: 1 addition & 0 deletions server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ storage/plugins/agent-skills/*
storage/plugins/agent-flows/*
storage/plugins/office-extensions/*
storage/plugins/anythingllm_mcp_servers.json
storage/push-notifications/*
!storage/documents/DOCUMENTS.md
logs/server.log
*.db
Expand Down
13 changes: 13 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const { browserExtensionEndpoints } = require("./endpoints/browserExtension");
const { communityHubEndpoints } = require("./endpoints/communityHub");
const { agentFlowEndpoints } = require("./endpoints/agentFlows");
const { mcpServersEndpoints } = require("./endpoints/mcpServers");
const { pushNotificationService } = require("./utils/PushNotifications");
const app = express();
const apiRouter = express.Router();
const FILE_LIMIT = "3GB";
Expand Down Expand Up @@ -66,6 +67,18 @@ communityHubEndpoints(apiRouter);
agentFlowEndpoints(apiRouter);
mcpServersEndpoints(apiRouter);

// Testing
apiRouter.post("/subscribe", (req, res) => {
const subscription = req.body;
pushNotificationService.subscribe(subscription);
res.status(201).json({});
});

apiRouter.get("/push-public-key", (_req, res) => {
const publicKey = pushNotificationService.publicVapidKey;
res.status(200).json({ publicKey });
});

// Externally facing embedder endpoints
embeddedEndpoints(apiRouter);

Expand Down
63 changes: 63 additions & 0 deletions server/jobs/push-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const { log, conclude } = require('./helpers/index.js');
const { pushNotificationService } = require('../utils/PushNotifications');
const { ApiChatHandler } = require('../utils/chats/apiChatHandler');
const { Workspace } = require('../models/workspace');
const { v4: uuidv4 } = require('uuid');

// Seen success with gemma3:1B with this format (limited testing)
function makeTitlePrompt(response) {
return `Generate a short title for a browser notification title based on the following content. Do not provide options or other text. Limit to 3-5 words:\n\n${response.textResponse}`;
}

function makeDescriptionPrompt(response) {
return `Summarize the following content into a browser notification message string - do not provide options or other text - just the content should be returned:\n\n${response.textResponse}`;
}

(async () => {
try {
console.log('Testing push notification service...');
const pushService = pushNotificationService.pushService;
if (!pushService) throw new Error('Failed to get push service');

const subscription = pushNotificationService.subscriptions?.[0];
if (!subscription) throw new Error('No subscription found');

const workspace = (await Workspace.where({}))[0]; // tmp
const message = '@agent open news.ycombinator.com and get me the latest news that might be interesting to read. I am specifically interested in the latest news about open source projects or AI related news.';

const chatPayload = {
workspace,
message,
mode: "chat",
user: null,
sessionId: uuidv4(),
}
const response = await ApiChatHandler.chatSync(chatPayload);
const titleResponse = await ApiChatHandler.chatSync({
workspace,
message: makeTitlePrompt(response),
mode: "chat",
user: null,
sessionId: uuidv4(),
}).then(r => r.textResponse);
const descriptionResponse = await ApiChatHandler.chatSync({
workspace,
message: makeDescriptionPrompt(response),
mode: "chat",
user: null,
sessionId: uuidv4(),
}).then(r => r.textResponse);

await pushService.sendNotification(subscription, JSON.stringify({
title: titleResponse,
message: descriptionResponse,
// onClickUrl: 'https://github.com/mintplex-labs/anything-llm'
}));
console.log('Successfully sent notification');
} catch (e) {
console.error(e)
log(`errored with ${e.message}`)
} finally {
conclude();
}
})();
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"uuid": "^9.0.0",
"uuid-apikey": "^1.5.3",
"weaviate-ts-client": "^1.4.0",
"web-push": "^3.6.7",
"winston": "^3.13.0"
},
"devDependencies": {
Expand All @@ -99,4 +100,4 @@
"nodemon": "^2.0.22",
"prettier": "^3.0.3"
}
}
}
22 changes: 16 additions & 6 deletions server/utils/BackgroundWorkers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class BackgroundService {
name = "BackgroundWorkerService";
static _instance = null;
#root = path.resolve(__dirname, "../../jobs");
#documentSyncEnabled = false;

constructor() {
if (BackgroundService._instance) {
Expand All @@ -24,8 +25,10 @@ class BackgroundService {

async boot() {
const { DocumentSyncQueue } = require("../../models/documentSyncQueue");
if (!(await DocumentSyncQueue.enabled())) {
this.#log("Feature is not enabled and will not be started.");
this.#documentSyncEnabled = await DocumentSyncQueue.enabled();

if (!this.jobs().length) {
this.#log("No jobs to run, schedule, or queue!");
return;
}

Expand Down Expand Up @@ -57,10 +60,17 @@ class BackgroundService {
return [
// Job for auto-sync of documents
// https://github.com/breejs/bree
{
name: "sync-watched-documents",
interval: "1hr",
},
...(this.#documentSyncEnabled ? [
{
name: "sync-watched-documents",
interval: "1hr",
},
] : []),
// {
// name: "push-test",
// // interval: "1m",
// timeout: "10s",
// },
];
}

Expand Down
125 changes: 125 additions & 0 deletions server/utils/PushNotifications/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const webpush = require("web-push");
const fs = require("fs");
const path = require("path");

class PushNotifications {
static mailTo = 'anythingllm@localhost';
/**
* @type {PushNotifications}
*/
static instance = null;

/**
* The VAPID keys for the push notification service.
* @type {{publicKey: string | null, privateKey: string | null}}
*/
#vapidKeys = {
publicKey: null,
privateKey: null,
};

/**
* The subscriptions for the push notification service.
* @type {Array<{Object}>}
*/
#subscriptions = [];

constructor() {
if (PushNotifications.instance) return PushNotifications.instance;
PushNotifications.instance = this;
}

#log(text, ...args) {
console.log(`\x1b[36m[PushNotifications]\x1b[0m ${text}`, ...args);
}

get pushService() {
try {
const vapidKeys = this.existingVapidKeys;
if (!vapidKeys.publicKey || !vapidKeys.privateKey) throw new Error('VAPID keys not found. Make sure they are generated in the main process first.');
webpush.setVapidDetails(
`mailto:${this.mailTo}`,
vapidKeys.publicKey,
vapidKeys.privateKey
);
return webpush;
} catch (e) {
console.error('Failed to set VAPID details', e);
return null;
}
}

get storagePath() {
return process.env.NODE_ENV === "development"
? path.resolve(__dirname, `../../storage`, 'push-notifications')
: path.resolve(process.env.STORAGE_DIR, 'push-notifications');
}

get existingVapidKeys() {
// Already loaded and binded to the instance
if (this.#vapidKeys.publicKey && this.#vapidKeys.privateKey) return this.#vapidKeys;

const vapidKeysPath = path.resolve(this.storagePath, `vapid-keys.json`);
if (!fs.existsSync(vapidKeysPath)) return { publicKey: null, privateKey: null };

const existingVapidKeys = JSON.parse(fs.readFileSync(vapidKeysPath, 'utf8'));
this.#log(`Loaded existing VAPID keys!`);
this.#vapidKeys.publicKey = existingVapidKeys.publicKey;
this.#vapidKeys.privateKey = existingVapidKeys.privateKey;
return this.#vapidKeys;
}

get publicVapidKey() {
return this.existingVapidKeys.publicKey;
}

get subscriptions() {
if (!fs.existsSync(path.resolve(this.storagePath, `subscriptions.json`))) return [];
const currentSubscriptions = JSON.parse(fs.readFileSync(path.resolve(this.storagePath, `subscriptions.json`), 'utf8'));
this.#subscriptions = currentSubscriptions;
return this.#subscriptions || [];
}

subscribe(subscription) {
console.log('new subscription ===================');
console.log(subscription);
console.log('end subscription ===================');
this.#subscriptions.push(subscription);

//tmp write this
let currentSubscriptions = []
currentSubscriptions.push(subscription);
fs.writeFileSync(path.resolve(this.storagePath, `subscriptions.json`), JSON.stringify(currentSubscriptions, null, 2));
this.#subscriptions = currentSubscriptions;
return this;
}

/**
* Setup the push notification service.
* This will generate new VAPID keys if they don't exist and save them to the storage path.
*/
static setupPushNotificationService() {
const instance = PushNotifications.instance;
const existingVapidKeys = instance.existingVapidKeys;
if (existingVapidKeys.publicKey && existingVapidKeys.privateKey) {
instance.pushService;
return;
}

instance.#log("Generating new VAPID keys...");
const vapidKeys = webpush.generateVAPIDKeys();
instance.#vapidKeys.publicKey = vapidKeys.publicKey;
instance.#vapidKeys.privateKey = vapidKeys.privateKey;
instance.#log(`New VAPID keys generated!`);
if (!fs.existsSync(instance.storagePath)) fs.mkdirSync(instance.storagePath, { recursive: true });
fs.writeFileSync(path.resolve(instance.storagePath, `vapid-keys.json`), JSON.stringify(vapidKeys, null, 2));

instance.pushService;
return;
}
}

module.exports = {
pushNotificationService: new PushNotifications(),
PushNotifications,
};
Loading