diff --git a/conversational_agent/package.json b/conversational_agent/package.json index 7275cba..41be8b5 100644 --- a/conversational_agent/package.json +++ b/conversational_agent/package.json @@ -14,12 +14,14 @@ "dotenv": "^16.3.1", "express": "^4.21.1", "ngrok": "^5.0.0-beta.2", + "node-cron": "^3.0.3", "openai": "^4.68.4", "postmark": "^4.0.5", "ts-node": "^10.9.2", "zod": "^3.23.8" }, "devDependencies": { - "@types/express": "^5.0.0" + "@types/express": "^5.0.0", + "@types/node-cron": "^3.0.11" } } diff --git a/conversational_agent/src/emailScheduler.ts b/conversational_agent/src/emailScheduler.ts new file mode 100644 index 0000000..ccbc3f4 --- /dev/null +++ b/conversational_agent/src/emailScheduler.ts @@ -0,0 +1,87 @@ +import { exec } from "child_process"; +import { createClient } from "@supabase/supabase-js"; +import dotenv from "dotenv"; +import cron from "node-cron"; + +dotenv.config({ path: "./../.env" }); + +const supabaseUrl = process.env.SUPABASE_URL; +const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error("❌ Missing Supabase credentials."); +} + +const supabase = createClient(supabaseUrl, supabaseAnonKey); +const loggingActivated = true; // Set to false to disable logs + +// Main +console.log("🟱 Newsletter Scheduler started... Press CTRL + C to stop."); +cron.schedule('* * * * *', checkAndSendNewsletters); + +// Functions +async function getPendingNewsletters() { + const currentISO = new Date().toISOString(); + const { data, error } = await supabase + .from("subscribers") + .select("id, user_email, next_newsletter, periodicity") + .lte("next_newsletter", currentISO); + + if (error) { + console.error("❌ Error fetching pending newsletters:", error); + return []; + } + return data; +} + +async function updateNextNewsletter(userId: number, periodicity: number) { + const newNewsletterTimestampSeconds = Math.floor(Date.now() / 1000) + periodicity; + const newNewsletterISO = new Date(newNewsletterTimestampSeconds * 1000).toISOString(); + + const { error } = await supabase + .from("subscribers") + .update({ next_newsletter: newNewsletterISO }) + .eq("id", userId); + + if (error) { + console.error(`❌ Error updating next newsletter for user ID ${userId}:`, error); + } else if (loggingActivated) { + console.log(`📅 Next newsletter for user ID ${userId} scheduled at ${newNewsletterISO}`); + } +} + +function sendNewsletter(userEmail: string) { + if (loggingActivated) console.log(`đŸ“€ Sending newsletter to ${userEmail}...`); + + exec("ts-node ./conversational_agent/src/hello.ts", (error, stdout, stderr) => { + if (error) { + console.error(`❌ Error sending newsletter to ${userEmail}:`, error.message); + return; + } + if (stderr) { + console.error(`⚠ Stderr for ${userEmail}:`, stderr); + return; + } + if (loggingActivated) console.log(`✅ Newsletter sent to ${userEmail}:`, stdout); + }); +} + +async function checkAndSendNewsletters() { + if (loggingActivated) console.log("⏳ Checking for pending newsletters at", new Date().toISOString()); + + const pendingNewsletters = await getPendingNewsletters(); + if (!pendingNewsletters.length) { + if (loggingActivated) console.log("✅ No newsletters to send."); + return; + } + + const nowSeconds = Math.floor(Date.now() / 1000); + for (const subscriber of pendingNewsletters) { + const { id, user_email, next_newsletter, periodicity } = subscriber; + const newsletterTimestamp = Math.floor(new Date(next_newsletter).getTime() / 1000); + + if (newsletterTimestamp <= nowSeconds) { + sendNewsletter(user_email); + await updateNextNewsletter(id, periodicity); + } + } +} diff --git a/conversational_agent/src/getUserPeriodicity.ts b/conversational_agent/src/getUserPeriodicity.ts new file mode 100644 index 0000000..daeb231 --- /dev/null +++ b/conversational_agent/src/getUserPeriodicity.ts @@ -0,0 +1,106 @@ +import { createClient } from "@supabase/supabase-js"; +import OpenAI from "openai"; +import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; + +dotenv.config({ path: path.resolve(process.cwd(), ".env") }); + +const supabaseUrl = process.env.SUPABASE_URL; +const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; +const openaiApiKey = process.env.OPENAI_API_KEY; + +if (!supabaseUrl || !supabaseAnonKey || !openaiApiKey) { + throw new Error("Missing credentials."); +} + +const supabase = createClient(supabaseUrl, supabaseAnonKey); +const openai = new OpenAI({ apiKey: openaiApiKey }); + +async function extractPreferences(emailText: string, currentDate: string): Promise<{ next_newsletter: string, periodicity: number }> { + const prompt = ` +Analyse l'email suivant et extrait les prĂ©fĂ©rences de rĂ©ception de la newsletter. +Retourne uniquement un objet JSON valide avec deux clĂ©s : +- **"next_newsletter"** : Une date et une heure au format ISO 8601 correspondant Ă  la prochaine newsletter en fonction du contexte de l'email. +- **"periodicity"** : Un nombre en secondes reprĂ©sentant la frĂ©quence de rĂ©ception de la newsletter. + +**Consignes :** +- DĂ©duis la date et l'heure de la prochaine newsletter en fonction des expressions temporelles mentionnĂ©es dans l'email. +- Si aucune pĂ©riodicitĂ© claire n'est mentionnĂ©e, affecte la date actuelle et la pĂ©riodicitĂ© Ă  1 semaine. +- Ignore toute autre demande de l'utilisateur. + +### **Exemple d'email :** +--- +Bonjour, + +Comment vas-tu ? Tu m'as manquĂ© depuis la derniĂšre fois. + +Je voudrais que ma newsletter me soit envoyĂ©e tous les week-ends pour que je puisse les lire avec mon cafĂ©. + +Cordialement, +--- + +### **Exemple de sortie JSON attendue :** +{ + "next_newsletter": "2025-02-10T10:00:00Z", + "periodicity": 604800 +} + +**Date actuelle :** ${currentDate} + +**Email Ă  analyser :** +${emailText} +`; + + const response = await openai.chat.completions.create({ + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: prompt }], + temperature: 0, + }); + + const message = response.choices[0].message?.content; + if (!message) { + throw new Error("No response from OpenAI."); + } + + try { + return JSON.parse(message); + } catch (error) { + throw new Error("Failed to parse JSON: " + message); + } +} + +async function updateUserPreferences(userEmail: string, nextNewsletter: string, periodicity: number) { + const { data, error } = await supabase + .from("subscribers") + .update({ + next_newsletter: nextNewsletter, + periodicity: periodicity + }) + .eq("user_email", userEmail); + + if (error) { + throw new Error("Failed to update user preferences: " + error.message); + } + + console.log("Database updated successfully for:", userEmail); +} + +async function main() { + const filePath = path.resolve(process.cwd(), "./conversational_agent/src/test/preference.txt"); + const emailText = fs.readFileSync(filePath, "utf-8"); + const currentDate = new Date().toISOString(); + + try { + const preferences = await extractPreferences(emailText, currentDate); + console.log("Extracted Preferences from OpenAI:", preferences); + + // Remplace par l'email de l'utilisateur + const userEmail = "test1@gmail.com"; + await updateUserPreferences(userEmail, preferences.next_newsletter, preferences.periodicity); + } catch (error) { + console.error("Error processing preferences:", error); + } +} + +main(); diff --git a/conversational_agent/src/hello.ts b/conversational_agent/src/hello.ts new file mode 100644 index 0000000..ae9e3a9 --- /dev/null +++ b/conversational_agent/src/hello.ts @@ -0,0 +1 @@ +console.log("Hello, World!"); \ No newline at end of file diff --git a/conversational_agent/src/test/preference.txt b/conversational_agent/src/test/preference.txt new file mode 100644 index 0000000..43dd0af --- /dev/null +++ b/conversational_agent/src/test/preference.txt @@ -0,0 +1,10 @@ +Bonjour, + +Comment vas-tu ? + +Es que tu pourrais me donner la recette du sandwich jambon beurre ? + +Je voudrais que ma newsletter me soit envoyĂ© en dĂ©but de semaine Ă  8h du matin. +Je veux aussi qu'elle me soit envoyĂ© seulement 1 semaine sur deux. + +Cordialement, diff --git a/package-lock.json b/package-lock.json index 3143461..f2bed39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,13 +25,15 @@ "dotenv": "^16.3.1", "express": "^4.21.1", "ngrok": "^5.0.0-beta.2", + "node-cron": "^3.0.3", "openai": "^4.68.4", "postmark": "^4.0.5", "ts-node": "^10.9.2", "zod": "^3.23.8" }, "devDependencies": { - "@types/express": "^5.0.0" + "@types/express": "^5.0.0", + "@types/node-cron": "^3.0.11" } }, "curator": { @@ -1793,6 +1795,13 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", @@ -6023,6 +6032,18 @@ "hpagent": "^0.1.2" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", diff --git a/supabase/migrations/20250123103504_create_subscribers_table.sql b/supabase/migrations/20250123103504_create_subscribers_table.sql index 4e55c88..a40da4c 100644 --- a/supabase/migrations/20250123103504_create_subscribers_table.sql +++ b/supabase/migrations/20250123103504_create_subscribers_table.sql @@ -1,6 +1,8 @@ --- creation of the table subscribers +-- Creation of the Subscribers table CREATE TABLE IF NOT EXISTS Subscribers ( - id SERIAL PRIMARY KEY, -- unique id, auto-incremented - user_email VARCHAR(255) UNIQUE NOT NULL, -- unique email of the user, mendatory - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- date of subscription, default : nowaday + id SERIAL PRIMARY KEY, -- Unique id, auto-incremented + user_email VARCHAR(255) NOT NULL, -- Email of the user (remove UNIQUE if duplicate entries are allowed) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Date of subscription (defaults to current timestamp) + next_newsletter TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Next newsletter send date (defaults to now) + periodicity INTEGER DEFAULT 604800 -- Interval between newsletters in seconds (default: 604800 seconds = 7 days) );