diff --git a/credential_stubs/mRqzxhSboGscolqI.json b/credential_stubs/mRqzxhSboGscolqI.json new file mode 100644 index 0000000..92bdd45 --- /dev/null +++ b/credential_stubs/mRqzxhSboGscolqI.json @@ -0,0 +1,23 @@ +{ + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL", + "type": "postgres", + "data": { + "host": "", + "port": 5432, + "database": "", + "user": "", + "password": "", + "ssl": true, + "sshTunnel": false, + "sslRejectUnauthorized": false, + "rejectUnauthorized": false + }, + "ownedBy": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/u0JCseXGnDG5hS9F.json b/credential_stubs/u0JCseXGnDG5hS9F.json new file mode 100644 index 0000000..fae5906 --- /dev/null +++ b/credential_stubs/u0JCseXGnDG5hS9F.json @@ -0,0 +1,16 @@ +{ + "id": "u0JCseXGnDG5hS9F", + "name": "Home Assistant API", + "type": "httpHeaderAuth", + "data": { + "name": "", + "value": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/tags.json b/tags.json index 04e9223..57c8086 100644 --- a/tags.json +++ b/tags.json @@ -15,6 +15,18 @@ { "id": "EWDBc4QSoIeOq7Q3", "name": "Documents" + }, + { + "id": "LYocedE5I1N5tEyg", + "name": "Behaviour" + }, + { + "id": "Ah5bIXYGdnykmxGo", + "name": "Media" + }, + { + "id": "Y2kqORepGSATiLQH", + "name": "Memory" } ], "mappings": [ @@ -30,10 +42,30 @@ "workflowId": "cOVKGH8x5PD0NZzA", "tagId": "lohEBeVbwLjFCYHK" }, + { + "workflowId": "o3uM1xDLTAKw4D6E", + "tagId": "Ah5bIXYGdnykmxGo" + }, + { + "workflowId": "o3uM1xDLTAKw4D6E", + "tagId": "Y2kqORepGSATiLQH" + }, + { + "workflowId": "K07e4PPANXDkmQsr", + "tagId": "Y2kqORepGSATiLQH" + }, + { + "workflowId": "K07e4PPANXDkmQsr", + "tagId": "Ah5bIXYGdnykmxGo" + }, { "workflowId": "1lIKvVJQIcva30YM", "tagId": "lohEBeVbwLjFCYHK" }, + { + "workflowId": "1lIKvVJQIcva30YM", + "tagId": "M3JFTtiZ0sxHInGG" + }, { "workflowId": "GBPFFq8rmbdFrNn9", "tagId": "M3JFTtiZ0sxHInGG" @@ -41,6 +73,22 @@ { "workflowId": "GBPFFq8rmbdFrNn9", "tagId": "EWDBc4QSoIeOq7Q3" + }, + { + "workflowId": "qtvB3r0cgejyCxUp", + "tagId": "0Cy9mOsSRGn0D1mL" + }, + { + "workflowId": "qtvB3r0cgejyCxUp", + "tagId": "M3JFTtiZ0sxHInGG" + }, + { + "workflowId": "JJ6B3w8i1bL7Q0rr", + "tagId": "M3JFTtiZ0sxHInGG" + }, + { + "workflowId": "JJ6B3w8i1bL7Q0rr", + "tagId": "lohEBeVbwLjFCYHK" } ] } \ No newline at end of file diff --git a/workflows/1lIKvVJQIcva30YM.json b/workflows/1lIKvVJQIcva30YM.json index c44ce82..ea436e1 100644 --- a/workflows/1lIKvVJQIcva30YM.json +++ b/workflows/1lIKvVJQIcva30YM.json @@ -31,8 +31,8 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - -1600, - 48 + -1568, + 144 ], "webhookId": "9959425d-bdbb-4597-ad72-77ec25fcf49c", "credentials": { @@ -54,25 +54,25 @@ "typeVersion": 1, "position": [ -1376, - 48 + 144 ] }, { "parameters": { "operation": "getAll", + "limit": 20, "filters": { - "readStatus": "unread", - "q": "-label:Processed" - }, - "limit": 20 + "q": "-label:Processed newer_than:1d", + "readStatus": "unread" + } }, "id": "b73f8915-1173-43a8-9f78-0c49fa471b7d", "name": "Gmail - Fetch ultime 3h", "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - 608, - 160 + -1008, + 304 ], "webhookId": "efc020f5-87c1-4009-8bc2-82ad371c0dde", "credentials": { @@ -92,8 +92,8 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - -944, - 48 + -736, + -32 ], "webhookId": "078abbbb-a75c-4507-8c0a-23196b1fef45", "credentials": { @@ -114,13 +114,12 @@ "type": "n8n-nodes-base.aggregate", "typeVersion": 1, "position": [ - -720, - 48 + -512, + -32 ] }, { "parameters": { - "mode": "runOnceForAllItems", "jsCode": "const item = $input.first();\nconst emailsRaw = item.json.emails || [];\nconst emails = emailsRaw.map(e => e.json || e);\n\nif (emails.length === 0) {\n return [{ json: { prompt: 'NESSUNA_EMAIL', emailCount: 0, emailMeta: [] } }];\n}\n\nconst emailMeta = emails.map(e => ({ id: e.id, threadId: e.threadId || e.id }));\n\nconst threads = {};\nemails.forEach(e => {\n const tid = e.threadId || e.id;\n if (!threads[tid]) threads[tid] = [];\n threads[tid].push(e.id);\n});\n\nconst emailList = emails.map((e, i) => {\n const from = e.From || e.from || 'Mittente sconosciuto';\n const subject = e.Subject || e.subject || '(nessun oggetto)';\n const date = e.Date || e.date || '';\n const body = (e.text || e.snippet || '').substring(0, 600);\n // Gmail node restituisce payload.mimeType='multipart/mixed' quando ci sono allegati\n const hasMixedPayload = e.payload && e.payload.mimeType === 'multipart/mixed';\n const hasAtt = (hasMixedPayload || (e.attachments && e.attachments.length > 0)) ? 'Sì (PDF probabile)' : 'No';\n const threadCount = (threads[e.threadId || e.id] || []).length;\n const threadNote = threadCount > 1 ? ` [THREAD: ${threadCount} msg]` : '';\n return `--- EMAIL ${i + 1}${threadNote} ---\nID: ${e.id}\nThreadID: ${e.threadId || e.id}\nDa: ${from}\nOggetto: ${subject}\nData: ${date}\nAllegati: ${hasAtt}\n${body}`;\n}).join('\\n\\n');\n\nconst today = new Date().toLocaleDateString('it-IT', {\n weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'\n});\n\nconst prompt = `Sei l'assistente personale di Martin. Analizza ${emails.length} email non lette.\n\nLABEL GMAIL (nomi ESATTI, rispetta maiuscole e &):\nLavoro/Comunicazioni, Lavoro/Cedolino, Lavoro/Contratto\nCondominio/Comunicazioni, Condominio/Spese\nInternet/Bollette, Internet/Comunicazioni\nLuce&Gas/Bollette, Luce&Gas/Comunicazioni\nMarketing, Prenotazioni\nRicevute/Trasporti, Ricevute/Acquisti, Ricevute/Abbonamenti\n\nREGOLE CLASSIFICAZIONE:\n- action \"trash\" solo per Marketing/newsletter → verranno eliminate automaticamente\n- action \"keep\" per tutto il resto → etichettate e incluse nel report\n- EMAIL con stesso ThreadID = stessa conversazione: riassumile INSIEME nel report\n- Non usare label non presenti nella lista sopra\n\nPAPERLESS ACTION (campo aggiuntivo, guarda il campo Allegati):\n- paperless_action \"pdf_allegato\" → email con allegato PDF che potrebbe valere la pena archiviare (Allegati: Sì) — bollette, ricevute, contratti, cedolini, comunicazioni ufficiali, etc.\n- paperless_action null → email senza allegati PDF rilevanti, o allegati irrilevanti (immagini, firma, etc.)\n\nREPORT (daily_report) - OBBLIGATORIO essere narrativo:\n- Scrivi come un assistente personale che racconta a Martin cosa c'è nella sua inbox\n- NON fare semplice elenco mittente/oggetto: racconta cosa è successo\n- Raggruppa thread correlati in un unico punto\n- Estrai dettagli concreti: importi €, date, numeri ordine, azioni richieste\n- Segnala esplicitamente se Martin deve fare qualcosa\n- Ordine: urgente/da rispondere, poi info utili, poi ricevute, poi eliminati\n- Usa *grassetto* e emoji Telegram. Max 3000 caratteri.\n\nRispondi SOLO JSON valido:\n{\n \"emails\": [\n {\"id\": \"ID_ESATTO\", \"action\": \"trash\", \"labels\": [\"Marketing\"], \"summary\": \"\", \"paperless_action\": null},\n {\"id\": \"ID_ESATTO\", \"action\": \"keep\", \"labels\": [\"Luce&Gas/Bollette\"], \"summary\": \"E.ON €87,50 scad. 28/03\", \"paperless_action\": \"pdf_allegato\"},\n {\"id\": \"ID_ESATTO\", \"action\": \"keep\", \"labels\": [\"Internet/Bollette\"], \"summary\": \"Avviso bolletta Fastweb disponibile\", \"paperless_action\": null}\n ],\n \"daily_report\": \"[REPORT NARRATIVO]\"\n}\n\n${emailList}`;\n\nreturn [{ json: { prompt, emailCount: emails.length, emailMeta } }];" }, "id": "f6b052dd-48da-41dd-8c68-f58c8b9f8c1d", @@ -128,13 +127,12 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -496, - 48 + -288, + -32 ] }, { "parameters": { - "mode": "runOnceForAllItems", "jsCode": "const raw = $input.first().json;\n// Formato OpenAI: choices[0].message.content\nconst rawText = (raw.choices && raw.choices[0] && raw.choices[0].message)\n ? raw.choices[0].message.content\n : (raw.text || raw.output || '');\n\nconst cleaned = rawText\n .replace(/[\\s\\S]*?<\\/thinking>/gi, '')\n .replace(/```json\\n?/g, '')\n .replace(/```\\n?/g, '')\n .trim();\n\nlet parsed;\ntry {\n parsed = JSON.parse(cleaned);\n} catch (e) {\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (match) {\n try { parsed = JSON.parse(match[0]); }\n catch (e2) { throw new Error('Parse error: ' + rawText.substring(0, 300)); }\n } else {\n throw new Error('No JSON found: ' + rawText.substring(0, 300));\n }\n}\n\nif (!parsed.emails || !Array.isArray(parsed.emails)) {\n throw new Error('Missing emails array: ' + JSON.stringify(parsed).substring(0, 200));\n}\n\nconst emailMeta = $('Costruisci Prompt').first().json.emailMeta || [];\nconst metaMap = {};\nemailMeta.forEach(m => { metaMap[m.id] = m.threadId; });\n\nparsed.emails = parsed.emails.map(e => ({\n ...e,\n threadId: metaMap[e.id] || e.id\n}));\n\nreturn [{ json: parsed }];" }, "id": "cac1efac-2d3e-482c-a529-22bdd2136575", @@ -142,8 +140,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -64, - 48 + 432, + -32 ] }, { @@ -159,8 +157,8 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - 160, - -96 + 704, + -192 ], "webhookId": "d42335fd-4748-4406-b0dd-d8d7f4d0b027", "credentials": { @@ -180,8 +178,8 @@ "type": "n8n-nodes-base.splitOut", "typeVersion": 1, "position": [ - 160, - 48 + 704, + 96 ] }, { @@ -200,8 +198,8 @@ "type": "n8n-nodes-base.if", "typeVersion": 1, "position": [ - 384, - 48 + 944, + 96 ] }, { @@ -214,8 +212,8 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - 608, - -96 + 1200, + -80 ], "webhookId": "abc3390b-9a83-4a40-bf36-d52a921a25b0", "credentials": { @@ -236,8 +234,8 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - 832, - -96 + 1488, + -80 ], "webhookId": "e0919d48-248a-4191-b8ca-6f9ff619bf16", "credentials": { @@ -249,7 +247,6 @@ }, { "parameters": { - "mode": "runOnceForAllItems", "jsCode": "const items = $input.all();\nif (items.length === 0) return [];\n\n// Costruisce mappa nome → ID dalle label Gmail\nconst labelsData = $('Gmail - Leggi tutte le label').all();\nconst labelMap = {};\nlabelsData.forEach(item => {\n const name = item.json.name;\n const id = item.json.id;\n if (name && id) labelMap[name] = id;\n});\n\n// Processa ogni email nella branch \"keep\"\nconst result = [];\nfor (const item of items) {\n const email = item.json;\n const requestedLabels = Array.isArray(email.labels) ? email.labels : [];\n const resolvedLabelIds = requestedLabels\n .map(name => labelMap[name])\n .filter(id => !!id);\n\n // Skip se nessuna label trovata (evita errore Gmail 'No label add or removes')\n if (resolvedLabelIds.length === 0) continue;\n\n result.push({\n json: {\n id: String(email.id || ''),\n threadId: String(email.threadId || email.id || ''),\n action: String(email.action || 'keep'),\n labels: requestedLabels,\n summary: String(email.summary || ''),\n resolvedLabelIds: resolvedLabelIds\n }\n });\n}\n\nreturn result;" }, "id": "99e0b420-3d9a-4ece-80ed-27dab248b018", @@ -257,8 +254,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 608, - 192 + 1504, + 368 ] }, { @@ -272,8 +269,8 @@ "type": "n8n-nodes-base.gmail", "typeVersion": 2.2, "position": [ - 832, - 192 + 1696, + 368 ], "webhookId": "debadb11-9b5c-44a6-9ad0-7f1a6598b14a", "credentials": { @@ -284,36 +281,24 @@ } }, { + "parameters": { + "path": "gmail-digest-test", + "options": {} + }, "id": "test-webhook-trigger", "name": "🧪 Test Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ -1824, - 200 + -112 ], - "parameters": { - "path": "gmail-digest-test", - "responseMode": "onReceived", - "options": {} - }, "webhookId": "gmail-digest-test-001" }, { - "id": "copilot-gpt41-node", - "name": "GPT-4.1 - Classifica Email", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - -336, - 48 - ], "parameters": { "method": "POST", "url": "https://api.githubcopilot.com/chat/completions", - "sendBody": true, - "options": {}, - "genericAuthType": "httpHeaderAuth", "sendHeaders": true, "headerParameters": { "parameters": [ @@ -335,29 +320,36 @@ } ] }, - "authentication": "none", + "sendBody": true, "contentType": "raw", "rawContentType": "application/json", - "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\": $('Costruisci Prompt').first().json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":8192}) }}" + "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\": $('Costruisci Prompt').first().json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":8192}) }}", + "options": {} }, - "credentials": {} - }, - { - "id": "copilot-token-refresh", - "name": "Ottieni Token Copilot", + "id": "copilot-gpt41-node", + "name": "GPT-4.1 - Classifica Email", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - -496, - 48 - ], + 192, + -32 + ] + }, + { "parameters": { - "method": "GET", "url": "https://api.github.com/copilot_internal/v2/token", "authentication": "predefinedCredentialType", "nodeCredentialType": "httpHeaderAuth", "options": {} }, + "id": "copilot-token-refresh", + "name": "Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -64, + -32 + ], "credentials": { "httpHeaderAuth": { "id": "vBwUxlzKrX3oDHyN", @@ -366,14 +358,6 @@ } }, { - "id": "if-paperless", - "name": "Ha azione Paperless?", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 384, - 250 - ], "parameters": { "conditions": { "options": { @@ -393,18 +377,19 @@ "rightValue": "" } ] - } - } - }, - { - "id": "if-bolletta-or-fastweb", - "name": "Ha PDF allegato?", + }, + "options": {} + }, + "id": "if-paperless", + "name": "Ha azione Paperless?", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ - 608, - 350 - ], + 1200, + 272 + ] + }, + { "parameters": { "conditions": { "options": {}, @@ -420,32 +405,32 @@ "rightValue": "pdf_allegato" } ] - } - } + }, + "options": {} + }, + "id": "if-bolletta-or-fastweb", + "name": "Ha PDF allegato?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 1488, + 112 + ] }, { + "parameters": { + "jsCode": "// Leggi il messaggio Gmail completo per trovare gli allegati PDF\n// Il nodo Gmail - Leggi contenuto ha già il payload completo\nconst emails = $input.all();\nconst results = [];\nfor (const emailItem of emails) {\n const email = emailItem.json;\n if (email.paperless_action !== 'pdf_allegato') continue;\n // Trova allegati PDF in payload.parts (ricorsivo)\n function findPDFs(parts, found) {\n if (!parts) return;\n for (const p of parts) {\n if (p.parts) findPDFs(p.parts, found);\n if (p.body && p.body.attachmentId) {\n const isPDF = (p.mimeType||'').includes('pdf') || (p.filename||'').toLowerCase().endsWith('.pdf');\n if (isPDF) found.push({ attachment_id: p.body.attachmentId, filename: p.filename || 'documento.pdf' });\n }\n }\n }\n const pdfs = [];\n findPDFs((email.payload && email.payload.parts) || [], pdfs);\n for (const pdf of pdfs) {\n results.push({ json: {\n email_id: email.id,\n attachment_id: pdf.attachment_id,\n filename: pdf.filename,\n hint: (email.Subject || email.subject || '') + ' da ' + (email.From || email.from || ''),\n from: email.From || email.from || '',\n }});\n }\n}\nreturn results;" + }, "id": "http-trigger-bolletta", "name": "Estrai Allegati PDF", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 832, - 260 - ], - "parameters": { - "mode": "runOnceForAllItems", - "jsCode": "// Leggi il messaggio Gmail completo per trovare gli allegati PDF\n// Il nodo Gmail - Leggi contenuto ha già il payload completo\nconst emails = $input.all();\nconst results = [];\nfor (const emailItem of emails) {\n const email = emailItem.json;\n if (email.paperless_action !== 'pdf_allegato') continue;\n // Trova allegati PDF in payload.parts (ricorsivo)\n function findPDFs(parts, found) {\n if (!parts) return;\n for (const p of parts) {\n if (p.parts) findPDFs(p.parts, found);\n if (p.body && p.body.attachmentId) {\n const isPDF = (p.mimeType||'').includes('pdf') || (p.filename||'').toLowerCase().endsWith('.pdf');\n if (isPDF) found.push({ attachment_id: p.body.attachmentId, filename: p.filename || 'documento.pdf' });\n }\n }\n }\n const pdfs = [];\n findPDFs((email.payload && email.payload.parts) || [], pdfs);\n for (const pdf of pdfs) {\n results.push({ json: {\n email_id: email.id,\n attachment_id: pdf.attachment_id,\n filename: pdf.filename,\n hint: (email.Subject || email.subject || '') + ' da ' + (email.From || email.from || ''),\n from: email.From || email.from || '',\n }});\n }\n}\nreturn results;" - } + 1824, + -16 + ] }, { - "id": "if-is-test", - "name": "È un test?", - "type": "n8n-nodes-base.if", - "typeVersion": 2, - "position": [ - 384, - 260 - ], "parameters": { "conditions": { "options": {}, @@ -460,26 +445,36 @@ } } ] - } - } + }, + "options": {} + }, + "id": "if-is-test", + "name": "È un test?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + -1184, + 144 + ] }, { + "parameters": { + "operation": "getAll", + "limit": 20, + "filters": { + "q": "-label:Processed newer_than:1d", + "readStatus": "unread" + } + }, "id": "gmail-fetch-all-unread", "name": "Gmail - Fetch tutte non lette", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ - 608, - 360 + -992, + -32 ], - "parameters": { - "operation": "getAll", - "filters": { - "readStatus": "unread", - "q": "-label:Processed" - }, - "limit": 20 - }, + "webhookId": "94c9342a-4f54-42f1-9f8f-4a165468ba33", "credentials": { "gmailOAuth2": { "id": "qvOikS6IF0H5khr8", @@ -488,14 +483,6 @@ } }, { - "id": "n_callcore", - "name": "Chiama Core Upload", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2200, - 200 - ], "parameters": { "method": "POST", "url": "https://orchestrator.mt-home.uk/webhook/paperless-upload", @@ -503,6 +490,79 @@ "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ email_id: $json.email_id, attachment_id: $json.attachment_id, filename: $json.filename, hint: $json.hint, from: $json.from }) }}", "options": {} + }, + "id": "n_callcore", + "name": "Chiama Core Upload", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2160, + 128 + ] + }, + { + "id": "n_estrai_fatti", + "name": "🧠 Estrai Fatti", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1900, + 400 + ], + "parameters": { + "jsCode": "const parsed = $input.first().json;\nconst copilotToken = $('Ottieni Token Copilot').first().json.token;\n\n// Only non-trash emails with a summary\nconst keepEmails = (parsed.emails || []).filter(e => e.action !== 'trash' && e.summary?.trim());\nif (keepEmails.length === 0) return [{ json: { _skip: true } }];\n\nconst emailList = keepEmails.map((e, i) =>\n `${i+1}. [labels: ${(e.labels||[]).join(',')}] [threadId: ${e.threadId}]\\nRiassunto: ${e.summary}`\n).join('\\n\\n');\n\nconst gptRes = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://api.githubcopilot.com/chat/completions',\n headers: {\n 'Authorization': `Bearer ${copilotToken}`,\n 'editor-version': 'vscode/1.95.3',\n 'Copilot-Integration-Id': 'vscode-chat',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model: 'gpt-4.1',\n messages: [\n { role: 'system', content: 'Sei un assistente che estrae fatti strutturati da email per il sistema di memoria di Pompeo, l\\'AI personale di Martin Tahiraj. Rispondi SEMPRE con JSON valido.' },\n { role: 'user', content: `Estrai un fatto strutturato per ogni email che Martin ha ricevuto oggi.\n\nEMAIL (indice. [label] [threadId]):\n${emailList}\n\nRegole TTL:\n- Prenotazioni, viaggi, eventi imminenti → ttl_days: 14\n- Bollette → ttl_days: 45\n- Lavoro, condominio, documenti ufficiali → ttl_days: 90\n- Abbonamenti, acquisti → ttl_days: 30\n- Default → ttl_days: 30\n\nRispondi SOLO JSON:\n{\n \"facts\": [\n {\n \"index\": 1,\n \"thread_id\": \"threadId_esatto\",\n \"category\": \"finance|work|home|personal|travel|subscription|health\",\n \"subject\": \"fatto conciso max 120 caratteri\",\n \"detail\": {\"from\": \"mittente\", \"labels\": [\"label1\"]},\n \"action_required\": false,\n \"action_text\": \"\",\n \"pompeo_note\": \"inner monologue breve: perché questo fatto è rilevante per capire Martin o la sua vita\",\n \"entity_refs\": {\"people\": [], \"places\": [], \"products\": [], \"amounts\": []},\n \"ttl_days\": 30\n }\n ]\n}` }\n ],\n temperature: 0.2,\n response_format: { type: 'json_object' }\n })\n});\n\nconst content = gptRes.choices?.[0]?.message?.content || '{}';\nconst { facts = [] } = JSON.parse(content);\n\nconst now = new Date();\nconst results = facts.map(f => {\n const email = keepEmails[f.index - 1] || keepEmails.find(e => e.threadId === f.thread_id);\n if (!f.thread_id && !email?.threadId) return null;\n const expiresAt = (f.ttl_days > 0)\n ? new Date(now.getTime() + f.ttl_days * 86400000).toISOString()\n : null;\n const detail = JSON.stringify({ ...(f.detail || {}), labels: email?.labels || [] });\n return { json: {\n thread_id: f.thread_id || email?.threadId,\n category: f.category || 'personal',\n subject: (f.subject || email?.summary || '').substring(0, 200).replace(/'/g, \"''\"),\n detail,\n action_required: f.action_required ? true : false,\n action_text: (f.action_text || '').replace(/'/g, \"''\"),\n pompeo_note: (f.pompeo_note || '').replace(/'/g, \"''\"),\n entity_refs: JSON.stringify(f.entity_refs || {}),\n expires_at: expiresAt\n }};\n}).filter(Boolean);\n\nreturn results.length > 0 ? results : [{ json: { _skip: true } }];" + } + }, + { + "id": "n_if_skip", + "name": "🔀 Ha Fatti?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 2120, + 400 + ], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "cond_skip", + "leftValue": "={{ $json._skip }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "notEquals" + } + } + ], + "combinator": "and" + } + } + }, + { + "id": "n_upsert_memoria", + "name": "💾 Upsert Memoria", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.5, + "position": [ + 2340, + 400 + ], + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + }, + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO memory_facts\n (user_id, source, source_ref, category, subject, detail, action_required, action_text, pompeo_note, entity_refs, expires_at)\nVALUES (\n 'martin',\n 'email',\n '{{ $json.thread_id }}',\n '{{ $json.category }}',\n '{{ $json.subject }}',\n '{{ $json.detail }}',\n {{ $json.action_required }},\n '{{ $json.action_text }}',\n '{{ $json.pompeo_note }}',\n '{{ $json.entity_refs }}'::jsonb,\n {{ $json.expires_at ? \"'\" + $json.expires_at + \"'::timestamptz\" : \"NULL\" }}\n)\nON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL DO UPDATE SET\n subject = EXCLUDED.subject,\n detail = EXCLUDED.detail,\n action_required = EXCLUDED.action_required,\n action_text = EXCLUDED.action_text,\n pompeo_note = EXCLUDED.pompeo_note,\n entity_refs = EXCLUDED.entity_refs,\n expires_at = EXCLUDED.expires_at", + "options": {} } } ], @@ -647,6 +707,11 @@ "node": "Dividi Email", "type": "main", "index": 0 + }, + { + "node": "🧠 Estrai Fatti", + "type": "main", + "index": 0 } ] ] @@ -759,6 +824,29 @@ } ] ] + }, + "🧠 Estrai Fatti": { + "main": [ + [ + { + "node": "🔀 Ha Fatti?", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔀 Ha Fatti?": { + "main": [ + [ + { + "node": "💾 Upsert Memoria", + "type": "main", + "index": 0 + } + ], + [] + ] } }, "settings": { @@ -767,7 +855,7 @@ "callerPolicy": "workflowsFromSameOwner" }, "triggerCount": 2, - "versionId": "55dea25a-a72c-4c4f-8126-cc6dae55e15a", + "versionId": "ba68ff52-7646-45ef-b999-af915df23a00", "owner": { "type": "personal", "projectId": "Hdttz401OqqtObPo", diff --git a/workflows/4ZIEGck9n4l5qaDt.json b/workflows/4ZIEGck9n4l5qaDt.json new file mode 100644 index 0000000..cd40efa --- /dev/null +++ b/workflows/4ZIEGck9n4l5qaDt.json @@ -0,0 +1,464 @@ +{ + "id": "4ZIEGck9n4l5qaDt", + "name": "📅 Pompeo — Calendar Agent [Schedule]", + "nodes": [ + { + "id": "5162caf6661f4aa8", + "name": "⏰ Schedule", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 0, + 300 + ], + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "*/30 * * * *" + } + ] + } + } + }, + { + "id": "63a2b1a9ba7a4cf2", + "name": "📅 Imposta Range", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 260, + 300 + ], + "parameters": { + "jsCode": "const now = new Date();\nconst start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).toISOString();\nconst end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7, 23, 59, 59).toISOString();\nreturn [{json: {start, end}}];" + } + }, + { + "id": "89ee9f58cd2f4d4d", + "name": "🔑 Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 520, + 300 + ], + "parameters": { + "url": "https://api.github.com/copilot_internal/v2/token", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "vBwUxlzKrX3oDHyN", + "name": "GitHub Copilot OAuth Token" + } + } + }, + { + "id": "ffb3d86cf2a54659", + "name": "📋 Prepara Calendari", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 780, + 300 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const start = $('📅 Imposta Range').first().json.start;\nconst end = $('📅 Imposta Range').first().json.end;\nconst HA = 'http://10.30.20.100:8123';\n\n// Note: calendar.films and calendar.serie_tv removed — handled by Media Agent (direct Radarr/Sonarr API)\nconst calendars = [\n {entity_id:'calendar.calendar', name:'Lavoro', category:'work', user_id:'martin'},\n {entity_id:'calendar.famiglia', name:'Famiglia', category:'personal',user_id:'martin'},\n {entity_id:'calendar.spazzatura', name:'Spazzatura', category:'chores', user_id:'martin'},\n {entity_id:'calendar.pulizie', name:'Pulizie', category:'chores', user_id:'martin'},\n {entity_id:'calendar.formula_1', name:'Formula 1', category:'leisure', user_id:'martin'},\n {entity_id:'calendar.lm_wec_fia_world_endurance_championship', name:'WEC', category:'leisure', user_id:'martin'},\n {entity_id:'calendar.inter_calendar', name:'Inter', category:'leisure', user_id:'martin'},\n {entity_id:'calendar.birthdays', name:'Compleanni', category:'social', user_id:'martin'},\n {entity_id:'calendar.varie', name:'Varie', category:'misc', user_id:'martin'},\n {entity_id:'calendar.festivita_in_italia', name:'Festività Italia',category:'holiday', user_id:'shared'}\n];\n\nreturn calendars.map(cal => ({json: {...cal, start, end, ha_base_url: HA}}));" + } + }, + { + "id": "7985f2d26aa64e69", + "name": "📡 HA - Scarica Calendario", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 1040, + 300 + ], + "parameters": { + "url": "={{ $json.ha_base_url + '/api/calendars/' + $json.entity_id + '?start=' + $json.start + '&end=' + $json.end }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "neverError": true + } + } + } + }, + "credentials": { + "httpHeaderAuth": { + "id": "u0JCseXGnDG5hS9F", + "name": "Home Assistant API" + } + }, + "onError": "continueRegularOutput" + }, + { + "id": "9ed15cad33234465", + "name": "🏷️ Estrai ed Etichetta", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1300, + 300 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const results = [];\nconst allCals = $('📋 Prepara Calendari').all();\n\nfor (let i = 0; i < $input.all().length; i++) {\n const httpItem = $input.all()[i];\n const pairedIdx = (typeof httpItem.pairedItem?.item === 'number') ? httpItem.pairedItem.item : i;\n const cal = allCals[pairedIdx]?.json ?? {};\n\n const events = Array.isArray(httpItem.json) ? httpItem.json : [];\n\n for (const ev of events) {\n const startVal = ev.start?.dateTime || ev.start?.date || null;\n const endVal = ev.end?.dateTime || ev.end?.date || null;\n const uid = ev.uid ||\n `${cal.entity_id}_${(ev.summary||'').replace(/\\s+/g,'_')}_${startVal}`;\n\n results.push({json: {\n uid,\n summary: ev.summary || '',\n description: ev.description || null,\n location: ev.location || null,\n start_dt: startVal,\n end_dt: endVal,\n all_day: !ev.start?.dateTime,\n calendar_name: cal.name || 'Unknown',\n calendar_entity: cal.entity_id || '',\n calendar_category:cal.category || 'misc',\n user_id: cal.user_id || 'martin'\n }});\n }\n}\n\nreturn results.length > 0 ? results : [{json:{skip:true}}];" + } + }, + { + "id": "9a2b47a3ce774ca1", + "name": "📝 Prepara Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1560, + 300 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const allItems = $input.all().map(i => i.json).filter(e => !e.skip);\nif (!allItems.length) return [{json:{skip:true, events:[], prompt:null, event_count:0}}];\n\n// Dedup by uid\nconst seen = new Set();\nconst events = allItems.filter(e => {\n if (!e.uid || seen.has(e.uid)) return false;\n seen.add(e.uid); return true;\n});\n\nconst fmt = dt => dt ? dt.substring(0,16).replace('T',' ') : '';\n\nconst eventList = events.map((e, idx) =>\n `${idx+1}. [${e.calendar_name}] ${e.summary}` +\n (e.all_day ? ' | Tutto il giorno' : ` | ${fmt(e.start_dt)} → ${fmt(e.end_dt)}`) +\n (e.location ? ` | 📍 ${e.location}` : '') +\n (e.description ? ` | ${e.description.substring(0,80)}` : '') +\n ` | uid: ${e.uid}`\n).join('\\n');\n\nconst prompt =\n`Sei Pompeo, l'assistente AI di Martin Tahiraj. Analizza questi eventi calendario (prossimi 7 giorni) e classificali.\n\nEVENTI:\n${eventList}\n\nRispondi SOLO con JSON valido nel formato:\n{\n \"events\": [\n {\n \"uid\": \"\",\n \"category\": \"work|personal|chores|leisure|social|holiday|misc\",\n \"action_required\": false,\n \"action_text\": null,\n \"do_not_disturb\": false,\n \"priority\": \"high|medium|low\",\n \"behavioral_context\": \"es: work_meeting|sport_event|home_chores|birthday\",\n \"home_presence_expected\": true,\n \"pompeo_note\": \"Nota breve in italiano per il briefing\"\n }\n ]\n}`;\n\nreturn [{json:{events, prompt, event_count: events.length}}];" + } + }, + { + "id": "5589a6ce6f1a4f81", + "name": "🤖 GPT-4.1 - Analizza Calendari", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [ + 1820, + 300 + ], + "parameters": { + "method": "POST", + "url": "https://api.githubcopilot.com/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('🔑 Ottieni Token Copilot').first().json.token }}" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Copilot-Integration-Id", + "value": "vscode-chat" + }, + { + "name": "Editor-Version", + "value": "vscode/1.85.0" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Sei Pompeo, l'assistente AI di Martin. Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\":$json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":2048}) }}", + "options": {} + } + }, + { + "id": "89505d194e014900", + "name": "📋 Parse Risposta GPT", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2080, + 300 + ], + "parameters": { + "jsCode": "const raw = $input.first().json;\nconst content = (raw.choices || [])[0]?.message?.content || '{}';\nlet parsed;\ntry { parsed = JSON.parse(content); }\ncatch(e) { throw new Error('GPT non JSON: ' + content.substring(0,300)); }\n\nconst gptEvents = parsed.events || [];\nconst originalEvents = $('📝 Prepara Prompt').first().json.events || [];\n\nif (!originalEvents.length) return [{json:{skip:true}}];\n\nreturn originalEvents.map((orig, i) => {\n const enriched = gptEvents.find(e => e.uid === orig.uid) || gptEvents[i] || {};\n return {json:{\n ...orig,\n category: enriched.category || orig.calendar_category || 'misc',\n action_required: enriched.action_required || false,\n action_text: enriched.action_text || null,\n do_not_disturb: enriched.do_not_disturb || false,\n priority: enriched.priority || 'low',\n behavioral_context: enriched.behavioral_context || null,\n home_presence_expected: enriched.home_presence_expected ?? null,\n pompeo_note: enriched.pompeo_note || orig.summary\n }};\n});" + } + }, + { + "id": "5bb69072497546d3", + "name": "💾 Postgres - Salva Evento", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 2860, + 300 + ], + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO memory_facts (user_id, source, source_ref, category, subject, detail, action_required, action_text, expires_at)\nVALUES (\n '{{ $json.user_id }}',\n 'calendar',\n '{{ $json.uid }}',\n '{{ $json.category }}',\n '{{ $json.summary }}',\n '{{ JSON.stringify({start:$json.start_dt,end:$json.end_dt,all_day:$json.all_day,location:$json.location,calendar:$json.calendar_name}) }}'::jsonb,\n {{ $json.action_required ? 'true' : 'false' }},\n '{{ $json.action_text || \"\" }}',\n '{{ $json.expires_at }}'::timestamptz\n)\nON CONFLICT ON CONSTRAINT memory_facts_dedup_idx DO UPDATE SET\n subject = EXCLUDED.subject,\n detail = EXCLUDED.detail,\n action_required = EXCLUDED.action_required,\n action_text = EXCLUDED.action_text,\n expires_at = EXCLUDED.expires_at", + "options": {} + }, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "id": "53f04d2895cf40b2", + "name": "📦 Aggrega Risultati", + "type": "n8n-nodes-base.aggregate", + "typeVersion": 1, + "position": [ + 3120, + 300 + ], + "parameters": { + "aggregate": "aggregateAllItemData", + "destinationFieldName": "events" + } + }, + { + "id": "5573e6e8d378427e", + "name": "✍️ Prepara Messaggio", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3380, + 300 + ], + "parameters": { + "jsCode": "const events = ($json.events || []).map(i => i.json || i).filter(e => !e.skip);\nif (!events.length) return [{json:{message:'📅 *Briefing Calendario*\\n\\nNessun evento nei prossimi 7 giorni.'}}];\n\nconst emojis = {work:'🗂',personal:'👨‍👩‍👧',chores:'🧹',leisure:'🏎',social:'🎂',holiday:'🎉',misc:'📌'};\nconst byCal = {};\nfor (const e of events) {\n const k = e.calendar_name || 'Varie';\n if (!byCal[k]) byCal[k] = {cat: e.calendar_category, evs:[]};\n byCal[k].evs.push(e);\n}\n\nconst today = new Date().toLocaleDateString('it-IT',{weekday:'long',day:'numeric',month:'long'});\nlet msg = `📅 *Briefing Calendario — Prossimi 7 giorni*\\n_(${today})_\\n\\n`;\n\nfor (const [cal, {cat, evs}] of Object.entries(byCal)) {\n msg += `${emojis[cat]||'📌'} *${cal}*\\n`;\n for (const e of evs) {\n const dt = e.all_day ? '' : (e.start_dt ? ' `' + e.start_dt.substring(11,16) + '`' : '');\n const flag = e.action_required ? ' ⚡' : '';\n msg += `• ${e.summary}${dt}${flag}\\n`;\n }\n msg += '\\n';\n}\nmsg += `_${events.length} eventi — generato da Pompeo_`;\nreturn [{json:{message:msg}}];" + } + }, + { + "id": "420db8250bf9492f", + "name": "📱 Telegram - Briefing Giornaliero", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1, + "position": [ + 3640, + 300 + ], + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "-4814221197", + "text": "={{ $json.message }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "id": "7fd81a1892184b61", + "name": "🗑️ Cleanup Cancellati", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 2340, + 300 + ], + "parameters": { + "operation": "executeQuery", + "query": "DELETE FROM memory_facts\nWHERE source = 'calendar'\n AND source_ref IS NOT NULL\n AND (detail->>'start')::timestamptz >= '{{ $('📅 Imposta Range').first().json.start }}'::timestamptz\n AND (detail->>'start')::timestamptz < '{{ $('📅 Imposta Range').first().json.end }}'::timestamptz\n AND source_ref NOT IN ({{ $('📋 Parse Risposta GPT').all().filter(i => i.json.uid).map(i => \"'\" + String(i.json.uid).replace(/'/g,\"''\") + \"'\").join(',') || \"'__no_uid__'\" }})", + "options": {} + }, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "id": "f8922396b255475a", + "name": "🔀 Riemetti Eventi", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2600, + 300 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "return $('📋 Parse Risposta GPT').all().filter(i => !i.json.skip);" + } + } + ], + "connections": { + "⏰ Schedule": { + "main": [ + [ + { + "node": "📅 Imposta Range", + "type": "main", + "index": 0 + } + ] + ] + }, + "📅 Imposta Range": { + "main": [ + [ + { + "node": "🔑 Ottieni Token Copilot", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔑 Ottieni Token Copilot": { + "main": [ + [ + { + "node": "📋 Prepara Calendari", + "type": "main", + "index": 0 + } + ] + ] + }, + "📋 Prepara Calendari": { + "main": [ + [ + { + "node": "📡 HA - Scarica Calendario", + "type": "main", + "index": 0 + } + ] + ] + }, + "📡 HA - Scarica Calendario": { + "main": [ + [ + { + "node": "🏷️ Estrai ed Etichetta", + "type": "main", + "index": 0 + } + ] + ] + }, + "🏷️ Estrai ed Etichetta": { + "main": [ + [ + { + "node": "📝 Prepara Prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "📝 Prepara Prompt": { + "main": [ + [ + { + "node": "🤖 GPT-4.1 - Analizza Calendari", + "type": "main", + "index": 0 + } + ] + ] + }, + "🤖 GPT-4.1 - Analizza Calendari": { + "main": [ + [ + { + "node": "📋 Parse Risposta GPT", + "type": "main", + "index": 0 + } + ] + ] + }, + "📋 Parse Risposta GPT": { + "main": [ + [ + { + "node": "🗑️ Cleanup Cancellati", + "type": "main", + "index": 0 + } + ] + ] + }, + "💾 Postgres - Salva Evento": { + "main": [ + [ + { + "node": "📦 Aggrega Risultati", + "type": "main", + "index": 0 + } + ] + ] + }, + "📦 Aggrega Risultati": { + "main": [ + [ + { + "node": "✍️ Prepara Messaggio", + "type": "main", + "index": 0 + } + ] + ] + }, + "✍️ Prepara Messaggio": { + "main": [ + [ + { + "node": "📱 Telegram - Briefing Giornaliero", + "type": "main", + "index": 0 + } + ] + ] + }, + "🗑️ Cleanup Cancellati": { + "main": [ + [ + { + "node": "🔀 Riemetti Eventi", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔀 Riemetti Eventi": { + "main": [ + [ + { + "node": "💾 Postgres - Salva Evento", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "5b03aaed-af76-4d6a-bff0-5a2949c24dfe", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file diff --git a/workflows/AyrKWvboPldzZPsM.json b/workflows/AyrKWvboPldzZPsM.json new file mode 100644 index 0000000..36da915 --- /dev/null +++ b/workflows/AyrKWvboPldzZPsM.json @@ -0,0 +1,260 @@ +{ + "id": "AyrKWvboPldzZPsM", + "name": "🎬 Pompeo — Jellyfin Playback [Webhook]", + "nodes": [ + { + "id": "wh_jellyfin", + "name": "Webhook Jellyfin", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "webhookId": "jellyfin-playback", + "position": [ + 0, + 300 + ], + "parameters": { + "httpMethod": "POST", + "path": "jellyfin-playback", + "responseMode": "onReceived", + "options": {} + } + }, + { + "id": "code_norm", + "name": "🔀 Normalizza Evento", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 280, + 300 + ], + "parameters": { + "jsCode": "// Parse body: il plugin Jellyfin invia il payload come stringa JSON dentro il body\nconst rawBody = $json.body || $json;\nconst body = typeof rawBody === 'string' ? JSON.parse(rawBody) : rawBody;\n\nconst notifType = body.NotificationType || body.Event || '';\nconst isStart = /PlaybackStart|Play$/i.test(notifType);\nconst isStop = /PlaybackStop|PlaybackPause|Pause|Stop|Scrobble/i.test(notifType);\nif (!isStart && !isStop) return [];\n\n// Whitelist utenti\nconst ALLOWED = ['42369255a7c64917a28fc26d4c7f8265', 'martin'];\nconst jellyUser = String(body.UserId || body.User || '');\nif (jellyUser && !ALLOWED.some(u => jellyUser.toLowerCase().includes(u.toLowerCase()))) return [];\n\n// Campi Jellyfin reali (plugin v1)\nconst series = body.SeriesName || null;\nconst season = body.SeasonNumber || null;\nconst ep = body.EpisodeNumber || null;\nconst item = body.Name || body.ItemName || 'Sconosciuto';\nconst device = body.DeviceName || body.Client || 'Unknown';\nconst sid = body.SessionId || body.DeviceId || null;\nconst itemType = (body.ItemType || '').toLowerCase();\n\nconst subject = series\n ? `${series} S${String(season||0).padStart(2,'0')}E${String(ep||0).padStart(2,'0')} - ${item}`\n : item;\n\nreturn [{json:{\n is_start: isStart,\n is_stop: isStop,\n subject,\n item_name: item,\n series_name: series,\n item_type: itemType,\n device,\n session_id: sid,\n user_id: 'martin',\n jellyfin_user_id: jellyUser,\n ts: new Date().toISOString()\n}}];" + } + }, + { + "id": "sw_startstop", + "name": "🔀 Start o Stop?", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [ + 560, + 300 + ], + "parameters": { + "mode": "rules", + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose" + }, + "combinator": "and", + "conditions": [ + { + "leftValue": "={{ $json.is_start }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true" + } + } + ] + }, + "renameOutput": true, + "outputKey": "start" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose" + }, + "combinator": "and", + "conditions": [ + { + "leftValue": "={{ $json.is_stop }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true" + } + } + ] + }, + "renameOutput": true, + "outputKey": "stop" + } + ] + } + } + }, + { + "id": "pg_start", + "name": "💾 PG - Inizia Sessione", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 840, + 180 + ], + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO behavioral_context (user_id,event_type,start_at,do_not_disturb,home_presence_expected,notes) VALUES ('{{ $json.user_id }}','watching_media',now(),true,true,'{{ JSON.stringify({item:$json.subject,device:$json.device,item_type:$json.item_type,session_id:$json.session_id}) }}'::jsonb)", + "options": {} + }, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "id": "pg_msg_start", + "name": "💬 PG - Msg Start", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 1120, + 180 + ], + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO agent_messages (agent,priority,event_type,user_id,subject,detail,expires_at) VALUES ('media','low','behavioral_observation','{{ $(\"🔀 Normalizza Evento\").item.json.user_id }}','▶️ {{ $(\"🔀 Normalizza Evento\").item.json.subject }} ({{ $(\"🔀 Normalizza Evento\").item.json.device }})','{{ JSON.stringify({event:\"watching_media_start\",item_type:$(\"🔀 Normalizza Evento\").item.json.item_type,device:$(\"🔀 Normalizza Evento\").item.json.device,ts:$(\"🔀 Normalizza Evento\").item.json.ts}) }}'::jsonb,now()+interval '4 hours')", + "options": {} + }, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "id": "pg_stop", + "name": "💾 PG - Chiudi Sessione", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 840, + 420 + ], + "parameters": { + "operation": "executeQuery", + "query": "UPDATE behavioral_context SET end_at=now(),do_not_disturb=false WHERE id=(SELECT id FROM behavioral_context WHERE user_id='{{ $json.user_id }}' AND event_type='watching_media' AND end_at IS NULL ORDER BY start_at DESC LIMIT 1)", + "options": {} + }, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "id": "pg_msg_stop", + "name": "💬 PG - Msg Stop", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 1120, + 420 + ], + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO agent_messages (agent,priority,event_type,user_id,subject,detail,expires_at) VALUES ('media','low','behavioral_observation','{{ $(\"🔀 Normalizza Evento\").item.json.user_id }}','⏹️ {{ $(\"🔀 Normalizza Evento\").item.json.subject }} ({{ $(\"🔀 Normalizza Evento\").item.json.device }})','{{ JSON.stringify({event:\"watching_media_stop\",device:$(\"🔀 Normalizza Evento\").item.json.device,ts:$(\"🔀 Normalizza Evento\").item.json.ts}) }}'::jsonb,now()+interval '1 hour')", + "options": {} + }, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + } + ], + "connections": { + "Webhook Jellyfin": { + "main": [ + [ + { + "node": "🔀 Normalizza Evento", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔀 Normalizza Evento": { + "main": [ + [ + { + "node": "🔀 Start o Stop?", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔀 Start o Stop?": { + "main": [ + [ + { + "node": "💾 PG - Inizia Sessione", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "💾 PG - Chiudi Sessione", + "type": "main", + "index": 0 + } + ] + ] + }, + "💾 PG - Inizia Sessione": { + "main": [ + [ + { + "node": "💬 PG - Msg Start", + "type": "main", + "index": 0 + } + ] + ] + }, + "💾 PG - Chiudi Sessione": { + "main": [ + [ + { + "node": "💬 PG - Msg Stop", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "fe285311-5816-456e-bdf1-b7f85fcc1f7c", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file diff --git a/workflows/GBPFFq8rmbdFrNn9.json b/workflows/GBPFFq8rmbdFrNn9.json index 7de18c7..15c8a36 100644 --- a/workflows/GBPFFq8rmbdFrNn9.json +++ b/workflows/GBPFFq8rmbdFrNn9.json @@ -51,7 +51,7 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -1952, + -1696, 0 ] }, @@ -66,7 +66,7 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - -1696, + -1440, 0 ], "webhookId": "ccbdaca8-c592-4c22-88a3-0e18b8272a26", @@ -110,8 +110,8 @@ "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ - -1696, - 464 + -1456, + 512 ] }, { @@ -411,8 +411,8 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - 2032, - 288 + 2048, + -16 ], "webhookId": "60fefda0-215b-49e8-86ae-15ef4b03d89f", "credentials": { @@ -431,8 +431,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 2032, - -64 + 2048, + 304 ] }, { @@ -469,8 +469,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 2288, - -64 + 2304, + 304 ], "credentials": { "httpHeaderAuth": { @@ -488,8 +488,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 2512, - -64 + 2528, + 304 ] }, { @@ -501,8 +501,8 @@ "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ - 2736, - -64 + 2752, + 304 ], "webhookId": "wait-pl-multi" }, @@ -527,8 +527,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 2960, - -64 + 2976, + 304 ], "credentials": { "httpHeaderAuth": { @@ -546,8 +546,8 @@ "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 3184, - -64 + 3200, + 304 ] }, { @@ -566,8 +566,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 3392, - -64 + 3408, + 304 ], "credentials": { "httpHeaderAuth": { @@ -589,8 +589,8 @@ "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [ - 3616, - -64 + 3632, + 304 ], "webhookId": "3be53ead-f628-4ec4-99d4-f378224c57ed", "credentials": { @@ -611,8 +611,8 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 3840, - -64 + 3856, + 304 ] }, { @@ -626,9 +626,43 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 4064, - -64 + 4080, + 304 ] + }, + { + "id": "n_memoria_code", + "name": "🧠 Salva in Memoria", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3200, + 400 + ], + "parameters": { + "jsCode": "// ─── 🧠 Salva in Memoria: memory_facts + Qdrant knowledge ────────────────────\n// Eseguito in parallelo dopo Paperless - Patch Metadati\n// Non bloccante: errori vengono loggati ma non interrompono il flusso\n\nconst OLLAMA_URL = 'http://ollama.ai.svc.cluster.local:11434';\nconst QDRANT_URL = 'http://qdrant.persistence.svc.cluster.local:6333';\nconst QDRANT_KEY = '__Montecarlo00!';\nconst COLLECTION = 'knowledge';\n\nconst parsed = $('Parse Risposta GPT').first().json;\nconst docData = $('Estrai Document ID').first().json;\nconst ocrText = ($('FileWizard - Stato OCR').first().json.result_preview || '').substring(0, 3000);\nconst normIn = $('Normalizza Input').first().json;\n\nconst docId = docData.doc_id;\nconst title = parsed.title || docData.title || normIn.filename || 'Documento';\nconst sourceRef = `paperless-${docId}`;\n\n// TTL by doc type\nconst dtype = (parsed.document_type_name || '').toLowerCase();\nlet ttlDays = 365;\nif (dtype.includes('bolletta') || dtype.includes('fattura')) ttlDays = 180;\nelse if (dtype.includes('cedolino')) ttlDays = 730;\nelse if (dtype.includes('ricevuta')) ttlDays = 90;\nconst expiresAt = new Date(Date.now() + ttlDays * 86400000).toISOString();\n\n// Text to embed: title + OCR excerpt\nconst embedText = `${title}\\n\\n${ocrText}`.substring(0, 4000);\n\n// ── 1. Embedding via Ollama ────────────────────────────────────────────────\nlet embedding = null;\ntry {\n const embedRes = await this.helpers.httpRequest({\n method: 'POST',\n url: `${OLLAMA_URL}/api/embed`,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ model: 'nomic-embed-text', input: embedText })\n });\n embedding = embedRes.embeddings?.[0] || null;\n} catch(e) {\n console.error('Ollama embed error:', e.message);\n}\n\n// ── 2. Qdrant upsert ───────────────────────────────────────────────────────\nlet qdrantId = null;\nif (embedding && embedding.length === 768) {\n const { v4: uuidv4 } = require('uuid');\n qdrantId = uuidv4();\n try {\n await this.helpers.httpRequest({\n method: 'PUT',\n url: `${QDRANT_URL}/collections/${COLLECTION}/points`,\n headers: { 'content-type': 'application/json', 'api-key': QDRANT_KEY },\n body: JSON.stringify({ points: [{\n id: qdrantId,\n vector: embedding,\n payload: {\n user_id: 'martin',\n source: 'paperless',\n source_ref: sourceRef,\n doc_id: docId,\n title,\n category: 'finance',\n doc_type: parsed.document_type_name || '',\n correspondent: parsed.correspondent_name || '',\n created_date: parsed.created_date || '',\n tags: Array.isArray(parsed.tag_ids) ? parsed.tag_ids : [],\n date: new Date().toISOString(),\n action_required: false\n }\n }]})\n });\n } catch(e) {\n console.error('Qdrant upsert error:', e.message);\n qdrantId = null;\n }\n}\n\n// ── 3. Prepare for Postgres node ──────────────────────────────────────────\nconst pompeoNote = `Documento \"${title}\" archiviato su Paperless (doc_id=${docId}). ` +\n `Tipo: ${parsed.document_type_name || 'N/D'}. ` +\n `Corrispondente: ${parsed.correspondent_name || 'N/D'}. ` +\n `Fonte: ${normIn._source === 'email' ? 'email da ' + (normIn.from || '?') : 'Telegram'}.`;\n\nconst detail = {\n paperless_doc_id: docId,\n doc_type: parsed.document_type_name || '',\n correspondent: parsed.correspondent_name || '',\n created_date: parsed.created_date || '',\n tag_ids: Array.isArray(parsed.tag_ids) ? parsed.tag_ids : [],\n source: normIn._source,\n is_duplicate: docData.is_duplicate || false,\n qdrant_embedded: !!qdrantId\n};\n\nreturn [{ json: {\n source_ref: sourceRef,\n category: 'finance',\n subject: title.substring(0, 200).replace(/'/g, \"''\"),\n detail: JSON.stringify(detail),\n action_required: false,\n action_text: '',\n pompeo_note: pompeoNote.substring(0, 500).replace(/'/g, \"''\"),\n entity_refs: JSON.stringify({ people: [], places: [], products: [], amounts: [] }),\n expires_at: expiresAt,\n qdrant_id: qdrantId\n}}];" + } + }, + { + "id": "n_pg_memoria", + "name": "💾 Upsert Memoria", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.5, + "position": [ + 3420, + 400 + ], + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + }, + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO memory_facts\n (user_id, source, source_ref, category, subject, detail, action_required, action_text, pompeo_note, entity_refs, expires_at)\nVALUES (\n 'martin',\n 'email',\n '{{ $json.thread_id }}',\n '{{ $json.category }}',\n '{{ $json.subject }}',\n '{{ $json.detail }}',\n {{ $json.action_required }},\n '{{ $json.action_text }}',\n '{{ $json.pompeo_note }}',\n '{{ $json.entity_refs }}'::jsonb,\n {{ $json.expires_at ? \"'\" + $json.expires_at + \"'::timestamptz\" : \"NULL\" }}\n)\nON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL DO UPDATE SET\n subject = EXCLUDED.subject,\n detail = EXCLUDED.detail,\n action_required = EXCLUDED.action_required,\n action_text = EXCLUDED.action_text,\n pompeo_note = EXCLUDED.pompeo_note,\n entity_refs = EXCLUDED.entity_refs,\n expires_at = EXCLUDED.expires_at", + "options": {} + } } ], "connections": { @@ -926,6 +960,11 @@ "node": "Telegram - Conferma Upload", "type": "main", "index": 0 + }, + { + "node": "🧠 Salva in Memoria", + "type": "main", + "index": 0 } ] ] @@ -951,6 +990,17 @@ } ] ] + }, + "🧠 Salva in Memoria": { + "main": [ + [ + { + "node": "💾 Upsert Memoria", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": { @@ -959,7 +1009,7 @@ "availableInMCP": false }, "triggerCount": 2, - "versionId": "5865a3df-1ea4-4983-a60e-cbc8997b89f8", + "versionId": "f0e2579d-f423-4465-a144-9f03572ba94e", "owner": { "type": "personal", "projectId": "Hdttz401OqqtObPo", diff --git a/workflows/JJ6B3w8i1bL7Q0rr.json b/workflows/JJ6B3w8i1bL7Q0rr.json new file mode 100644 index 0000000..a26a3af --- /dev/null +++ b/workflows/JJ6B3w8i1bL7Q0rr.json @@ -0,0 +1,135 @@ +{ + "id": "JJ6B3w8i1bL7Q0rr", + "name": "💬 Pompeo - Alexa Voice Interface [Webhook]", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "pompeo-alexa", + "responseMode": "lastNode", + "options": {} + }, + "id": "n_wh", + "name": "Alexa Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 208, + 304 + ], + "webhookId": "pompeo-alexa" + }, + { + "parameters": { + "operation": "executeQuery", + "query": "SELECT subject, category, pompeo_note FROM memory_facts WHERE user_id='martin' AND (expires_at IS NULL OR expires_at > now()) ORDER BY created_at DESC LIMIT 15", + "options": {} + }, + "id": "n_memory", + "name": "Leggi Memoria", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.5, + "position": [ + 432, + 304 + ], + "alwaysOutputData": true, + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "parameters": { + "url": "https://api.github.com/copilot_internal/v2/token", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer ghu_WetWTL0IOKvvUPwZk6RGZ2qBnPd5Nz47qPXM" + }, + { + "name": "editor-version", + "value": "vscode/1.95.3" + } + ] + }, + "options": {} + }, + "id": "n_token", + "name": "Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 640, + 304 + ] + }, + { + "parameters": { + "jsCode": "const webhookItem = $('Alexa Webhook').first().json;\nconst headers = webhookItem.headers || {};\nif ((headers['x-n8n-webhook-secret'] || '') !== 'cXvt0LZK9koIqgzcJZgV2qb4ymlEwpa7') {\n return [{ json: { tts_response: 'Accesso non autorizzato.' } }];\n}\nconst body = webhookItem.body || webhookItem;\nconst requestType = body.request?.type || 'Unknown';\nconst intent = body.request?.intent?.name || '';\nconst slots = body.request?.intent?.slots || {};\nconst query = Object.values(slots).map(s => s?.value).filter(Boolean).join(' ') || '';\n\nif (requestType === 'LaunchRequest') return [{ json: { tts_response: 'Ciao Martin! Sono Pompeo, il tuo assistente. Dimmi pure!' } }];\nif (requestType === 'SessionEndedRequest') return [{ json: { tts_response: '' } }];\nif (['AMAZON.StopIntent','AMAZON.CancelIntent'].includes(intent)) return [{ json: { tts_response: 'A presto Martin!' } }];\nif (intent === 'AMAZON.HelpIntent') return [{ json: { tts_response: 'Puoi chiedermi qualsiasi cosa. Cosa vuoi sapere?' } }];\n\nconst memoryFacts = $('Leggi Memoria').all().map(i => i.json).filter(f => f.subject);\nconst copilotToken = $('Ottieni Token Copilot').first().json.token;\nconst today = new Date().toLocaleDateString('it-IT', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\nconst memCtx = memoryFacts.length > 0\n ? memoryFacts.map(f => `- [${f.category}] ${f.subject}${f.pompeo_note ? ' -> ' + f.pompeo_note : ''}`).join('\\n')\n : '(nessun fatto recente)';\n\nconst gptResp = await this.helpers.httpRequest({\n method: 'POST', url: 'https://api.githubcopilot.com/chat/completions',\n headers: { 'Authorization': `Bearer ${copilotToken}`, 'Content-Type': 'application/json',\n 'editor-version': 'vscode/1.95.3', 'Copilot-Integration-Id': 'vscode-chat' },\n body: JSON.stringify({ model: 'gpt-4.1', messages: [\n { role: 'system', content: `Sei Pompeo, l'assistente AI personale di Martin Tahiraj. Rispondi via Amazon Echo (testo letto ad alta voce). Regole: italiano, max 2-3 frasi concise, niente markdown o emoji, tono amichevole. Oggi: ${today}\\nContesto:\\n${memCtx}` },\n { role: 'user', content: query || 'dimmi qualcosa su di me' }\n ], temperature: 0.7, max_tokens: 200 })\n});\nconst content = gptResp.choices?.[0]?.message?.content || 'Non ho capito, puoi ripetere?';\nreturn [{ json: { tts_response: content.replace(/\\*\\*(.*?)\\*\\*/g,'$1').replace(/\\*(.*?)\\*/g,'$1').replace(/#{1,6}\\s/g,'').trim() } }];" + }, + "id": "n_master", + "name": "Pompeo Core", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 864, + 304 + ] + } + ], + "connections": { + "Alexa Webhook": { + "main": [ + [ + { + "node": "Leggi Memoria", + "type": "main", + "index": 0 + } + ] + ] + }, + "Leggi Memoria": { + "main": [ + [ + { + "node": "Ottieni Token Copilot", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ottieni Token Copilot": { + "main": [ + [ + { + "node": "Pompeo Core", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "ea3ed67a-87ab-4289-8f53-c7e083e08087", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file diff --git a/workflows/K07e4PPANXDkmQsr.json b/workflows/K07e4PPANXDkmQsr.json new file mode 100644 index 0000000..5660f27 --- /dev/null +++ b/workflows/K07e4PPANXDkmQsr.json @@ -0,0 +1,337 @@ +{ + "id": "K07e4PPANXDkmQsr", + "name": "🎞️ Pompeo — Jellyfin Watch History [Schedule]", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "triggerAtHour": 4 + } + ] + } + }, + "id": "b76a6172-d497-42a5-b286-56263e0de523", + "name": "⏰ Cron", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 0, + 304 + ] + }, + { + "parameters": { + "url": "http://jellyfin.media.svc.cluster.local:8096/Users/42369255a7c64917a28fc26d4c7f8265/Items?Recursive=true&IncludeItemTypes=Movie,Episode&Fields=Genres,UserData,SeriesName,ParentIndexNumber&SortBy=DatePlayed&SortOrder=Descending&Limit=100", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "MediaBrowser Token=\"d153606c1ca54574a20d2b40fcf1b02e\"" + } + ] + }, + "options": {} + }, + "id": "6bfda9a6-3b1f-46cb-ac05-0c19b9720e30", + "name": "🎞️ HTTP Jellyfin", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 224, + 304 + ] + }, + { + "parameters": { + "jsCode": "const response = $input.first().json;\nconst allItems = response.Items || [];\n\nconst now = new Date();\nconst ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);\n\nconst filtered = allItems\n .filter(d => d.UserData && d.UserData.PlayCount > 0)\n .filter(d => {\n if (!d.UserData.LastPlayedDate) return false;\n return new Date(d.UserData.LastPlayedDate) >= ninetyDaysAgo;\n })\n .map(d => ({\n title: d.Type === 'Episode'\n ? (d.SeriesName || '') + ' S' + String(d.ParentIndexNumber || '')\n : (d.Name || ''),\n type: d.Type === 'Episode' ? 'episode' : 'movie',\n genres: d.Genres || [],\n play_count: d.UserData.PlayCount,\n played_pct: d.UserData.PlayedPercentage || 0,\n last_played: d.UserData.LastPlayedDate,\n is_favorite: d.UserData.IsFavorite || false\n }));\n\nlet watch_summary = 'Contenuti visti di recente:\\n';\nfor (const item of filtered) {\n const dateStr = item.last_played\n ? new Date(item.last_played).toLocaleDateString('it-IT')\n : 'N/A';\n watch_summary += '- ' + item.title + ' (' + item.type + ', ' + dateStr + ') - ' + item.play_count + 'x\\n';\n}\n\nreturn [{ json: { items: filtered, watch_summary } }];" + }, + "id": "11e8a16f-4b77-4d1e-987b-3b269d577f08", + "name": "🔀 Filtra Visti", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 448, + 304 + ] + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $json.items.length }}", + "operation": "larger" + } + ] + } + }, + "id": "19de4739-efe2-495e-969a-fbb01cbd7eeb", + "name": "❓ Ha Visti?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 672, + 304 + ] + }, + { + "parameters": {}, + "id": "d0a1c6e7-06eb-4d74-ac8b-c3c18abbcdbb", + "name": "⛔ Stop", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 880, + 480 + ] + }, + { + "parameters": { + "url": "https://api.github.com/copilot_internal/v2/token", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "id": "52f1351c-594b-4e33-8c50-5318b7de67ff", + "name": "🔑 Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 880, + 192 + ], + "credentials": { + "httpHeaderAuth": { + "id": "vBwUxlzKrX3oDHyN", + "name": "GitHub Copilot OAuth Token" + } + } + }, + { + "parameters": { + "jsCode": "const data = $input.first().json;\nconst watch_summary = data.watch_summary;\n\nconst prompt = 'Analizza questi contenuti guardati di recente da Martin e restituisci un JSON:\\n' +\n '{\\n' +\n ' \"recent_favorites\": [\"titoli\", \"più\", \"visti\"],\\n' +\n ' \"preferred_genres\": [\"generi\", \"prevalenti\"],\\n' +\n ' \"watch_patterns\": \"descrizione in italiano dei pattern di visione\",\\n' +\n ' \"completion_rate\": \"alta|media|bassa (basato su played_pct medio)\",\\n' +\n ' \"notes\": \"osservazioni utili per l\\'assistente personale\"\\n' +\n '}\\n\\n' +\n 'Cronologia:\\n' + watch_summary;\n\nreturn [{ json: { prompt } }];" + }, + "id": "5d4d889c-bda7-4f7e-8873-277f2b8727d5", + "name": "📝 Build Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1104, + 192 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.githubcopilot.com/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('🔑 Token Copilot').first().json.token }}" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Copilot-Integration-Id", + "value": "vscode-chat" + }, + { + "name": "Editor-Version", + "value": "vscode/1.85.0" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\": $('📝 Build Prompt').first().json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":1024}) }}", + "options": {} + }, + "id": "5135bca5-5f8b-4464-83d1-495ec9855a4a", + "name": "🤖 GPT-4.1", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1328, + 192 + ] + }, + { + "parameters": { + "jsCode": "const content = $input.first().json.choices[0].message.content;\nreturn [{ json: JSON.parse(content) }];" + }, + "id": "8dd4c239-79c2-47de-b898-f0f3d5b00e40", + "name": "🔍 Parse", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1552, + 192 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO memory_facts (user_id, source, category, subject, detail, expires_at, source_ref)\nVALUES (\n 'martin',\n 'jellyfin',\n 'watch_history',\n 'Cronologia Visione Recente',\n '{{ $json.detail_json }}'::jsonb,\n NOW() + INTERVAL '30 days',\n 'watch_history_summary'\n)\nON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL\nDO UPDATE SET\n detail = EXCLUDED.detail,\n expires_at = EXCLUDED.expires_at,\n created_at = NOW();", + "options": {} + }, + "id": "93ca06bb-5a19-4931-a023-e701b367ce58", + "name": "💾 PG — Upsert Cronologia", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 1904, + 192 + ], + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "parameters": { + "jsCode": "const parsed = $input.first().json;\nconst detailJson = JSON.stringify(parsed).split(\"'\").join(\"''\");\nreturn [{ json: { detail_json: detailJson } }];" + }, + "id": "599a48c0-dc77-4699-9060-ebccec1cc92c", + "name": "🔧 Prepara Detail B2", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1728, + 192 + ] + } + ], + "connections": { + "⏰ Cron": { + "main": [ + [ + { + "node": "🎞️ HTTP Jellyfin", + "type": "main", + "index": 0 + } + ] + ] + }, + "🎞️ HTTP Jellyfin": { + "main": [ + [ + { + "node": "🔀 Filtra Visti", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔀 Filtra Visti": { + "main": [ + [ + { + "node": "❓ Ha Visti?", + "type": "main", + "index": 0 + } + ] + ] + }, + "❓ Ha Visti?": { + "main": [ + [ + { + "node": "🔑 Token Copilot", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "⛔ Stop", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔑 Token Copilot": { + "main": [ + [ + { + "node": "📝 Build Prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "📝 Build Prompt": { + "main": [ + [ + { + "node": "🤖 GPT-4.1", + "type": "main", + "index": 0 + } + ] + ] + }, + "🤖 GPT-4.1": { + "main": [ + [ + { + "node": "🔍 Parse", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔍 Parse": { + "main": [ + [ + { + "node": "🔧 Prepara Detail B2", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔧 Prepara Detail B2": { + "main": [ + [ + { + "node": "💾 PG — Upsert Cronologia", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "bbcf28d7-652d-4aa9-b2a0-74d5ec29b56c", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file diff --git a/workflows/ZX5rLSETg6Xcymps.json b/workflows/ZX5rLSETg6Xcymps.json new file mode 100644 index 0000000..14a16de --- /dev/null +++ b/workflows/ZX5rLSETg6Xcymps.json @@ -0,0 +1,857 @@ +{ + "id": "ZX5rLSETg6Xcymps", + "name": "📄 Paperless — Upload Documento [Telegram]", + "nodes": [ + { + "id": "n01", + "name": "Telegram Trigger", + "type": "n8n-nodes-base.telegramTrigger", + "typeVersion": 1.1, + "position": [ + -2000, + 0 + ], + "parameters": { + "updates": [ + "message" + ], + "additionalFields": {} + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + }, + "webhookId": "tg-documento-v3" + }, + { + "id": "n02", + "name": "Check Caption", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1750, + 0 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const msg = $input.first().json.message;\nconst doc = msg.document;\nif (!doc) return [];\nconst caption = (msg.caption || '').trim();\nif (!caption.toLowerCase().startsWith('documento')) return [];\nreturn [{ json: {\n file_id: doc.file_id,\n filename: doc.file_name || 'documento.pdf',\n mime_type: doc.mime_type || 'application/pdf',\n caption,\n chat_id: String(msg.chat.id),\n}}];" + } + }, + { + "id": "n03new", + "name": "Telegram - Scarica File", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + -1500, + 0 + ], + "parameters": { + "resource": "file", + "operation": "get", + "fileId": "={{ $json.file_id }}", + "download": true, + "binaryProperty": "attachment", + "additionalFields": {} + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "id": "n05", + "name": "FileWizard - Avvia OCR", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -1000, + 0 + ], + "parameters": { + "method": "POST", + "url": "http://filewizard.home.svc.cluster.local:8000/ocr-pdf", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "parameterType": "formBinaryData", + "name": "file", + "inputDataFieldName": "data" + } + ] + }, + "options": {} + } + }, + { + "id": "n06", + "name": "⏳ Attendi OCR", + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + -750, + 0 + ], + "parameters": { + "amount": 25, + "unit": "seconds" + }, + "webhookId": "wait-ocr-tg" + }, + { + "id": "n07", + "name": "FileWizard - Stato OCR", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -500, + 0 + ], + "parameters": { + "method": "GET", + "url": "=http://filewizard.home.svc.cluster.local:8000/job/{{ $json.job_id }}", + "options": {} + } + }, + { + "id": "n08", + "name": "Paperless - Corrispondenti", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -250, + 0 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/correspondents/?page_size=50", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n09", + "name": "Paperless - Tipi Doc", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 0, + 0 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/document_types/?page_size=50", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n10", + "name": "Paperless - Tag", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 250, + 0 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/tags/?page_size=50", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n11", + "name": "Paperless - Percorsi", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 500, + 0 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/storage_paths/?page_size=50", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n12", + "name": "Build Prompt", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 750, + 0 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const fi = $('Check Caption').first().json;\nconst ocr = ($('FileWizard - Stato OCR').first().json.result_preview || '').substring(0, 2500);\nconst C = ($('Paperless - Corrispondenti').first().json.results || []).map(c => '[' + c.id + '] ' + c.name + ' (' + (c.document_count||0) + ' doc)').join('\\n');\nconst D = ($('Paperless - Tipi Doc').first().json.results || []).map(d => '[' + d.id + '] ' + d.name).join('\\n');\nconst T = ($('Paperless - Tag').first().json.results || []).map(t => '[' + t.id + '] ' + t.name).join('\\n');\nconst P = ($('Paperless - Percorsi').first().json.results || []).map(s => '[' + s.id + '] ' + s.name).join('\\n');\nconst prompt = `Sei l'assistente di Martin che gestisce Paperless-NGX.\nHa inviato un PDF via Telegram con caption: \"${fi.caption}\".\nNome file: ${fi.filename}\n\nTESTO ESTRATTO DAL PDF (OCR):\n${ocr}\n\nCORRISPONDENTI DISPONIBILI:\n${C}\n\nTIPI DOCUMENTO:\n${D}\n\nTAG:\n${T}\n\nPERCORSI:\n${P}\n\nISTRUZIONI:\n1. Se il documento non corrisponde a nessun tipo/tag esistente: skip:true con motivazione\n2. Altrimenti inferisci i metadati dal testo OCR (importo, periodo, data emissione...)\n3. Titolo descrittivo con dettagli concreti (es: 'E.ON Bolletta Energia Mar2026 87.50EUR')\n4. created_date = data emissione dal documento (non oggi)\n\nRispondi SOLO JSON valido:\n{\\\"skip\\\":false,\\\"skip_reason\\\":\\\"\\\",\\\"correspondent_id\\\":44,\\\"document_type_id\\\":2,\\\"tag_ids\\\":[3],\\\"storage_path_id\\\":2,\\\"title\\\":\\\"...\\\",\\\"created_date\\\":\\\"2026-03-01\\\",\\\"confidence_note\\\":\\\"...\\\"}`;\nreturn [{ json: { prompt, filename: fi.filename, caption: fi.caption }}];" + } + }, + { + "id": "n13", + "name": "Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1000, + 0 + ], + "parameters": { + "method": "GET", + "url": "https://api.github.com/copilot_internal/v2/token", + "options": {}, + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth" + }, + "credentials": { + "httpHeaderAuth": { + "id": "vBwUxlzKrX3oDHyN", + "name": "GitHub Copilot OAuth Token" + } + } + }, + { + "id": "n14", + "name": "GPT-4.1 - Inferisci Metadati", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1250, + 0 + ], + "parameters": { + "method": "POST", + "url": "https://api.githubcopilot.com/chat/completions", + "authentication": "none", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('Ottieni Token Copilot').first().json.token }}" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Copilot-Integration-Id", + "value": "vscode-chat" + }, + { + "name": "Editor-Version", + "value": "vscode/1.85.0" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\": $('Build Prompt').first().json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":1024}) }}", + "options": {} + } + }, + { + "id": "n15", + "name": "Parse Risposta GPT", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1500, + 0 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const raw = $input.first().json;\nconst content = (raw.choices || [])[0]?.message?.content || '{}';\nlet meta;\ntry { meta = JSON.parse(content); } catch(e) { throw new Error('GPT non JSON: ' + content.substring(0,300)); }\nconst fi = $('Check Caption').first().json;\nreturn [{ json: { ...meta, filename: fi.filename, caption: fi.caption }}];" + } + }, + { + "id": "n16", + "name": "Skip?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 1750, + 0 + ], + "parameters": { + "conditions": { + "options": {}, + "combinator": "and", + "conditions": [ + { + "id": "s1", + "leftValue": "={{ $json.skip }}", + "operator": { + "type": "boolean", + "operation": "true" + }, + "rightValue": "" + } + ] + } + } + }, + { + "id": "n17", + "name": "Telegram - Documento Non Riconosciuto", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1980, + 140 + ], + "parameters": { + "chatId": "-4814221197", + "text": "={{ '⚠️ *Documento non archiviato*\\n\\n' + $json.skip_reason + '\\n\\nCaricalo manualmente su Paperless se necessario.' }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "id": "n18", + "name": "Prepara Upload", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1980, + -140 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const meta = $('Parse Risposta GPT').first().json;\nconst dl = $('Telegram - Scarica File').first();\nconst filename = meta.filename || 'documento.pdf';\nconst titleFromFilename = filename.replace(/\\.[^/.]+$/, '');\nlet tag_ids = meta.tag_ids;\nif (typeof tag_ids === 'string') { try { tag_ids = JSON.parse(tag_ids); } catch(e) { tag_ids = []; } }\nconst created_date = meta.created_date && meta.created_date !== 'None'\n ? String(meta.created_date).substring(0, 10)\n : new Date().toISOString().substring(0, 10);\nreturn [{ json: {\n title: meta.title || titleFromFilename,\n upload_title: titleFromFilename,\n filename,\n correspondent_id: meta.correspondent_id != null ? Number(meta.correspondent_id) : null,\n document_type_id: meta.document_type_id != null ? Number(meta.document_type_id) : null,\n storage_path_id: meta.storage_path_id != null ? Number(meta.storage_path_id) : null,\n tag_ids: Array.isArray(tag_ids) ? tag_ids.map(Number).filter(n => !isNaN(n)) : [],\n created_date,\n}, binary: dl.binary }];" + } + }, + { + "id": "n19", + "name": "Paperless - Upload Documento", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2220, + -140 + ], + "parameters": { + "method": "POST", + "url": "https://docs.mt-home.uk/api/documents/post_document/", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "parameterType": "formBinaryData", + "name": "document", + "inputDataFieldName": "data" + }, + { + "name": "title", + "value": "={{ $json.upload_title }}" + } + ] + }, + "options": { + "response": { + "response": { + "responseFormat": "text" + } + } + } + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n20", + "name": "Estrai Task ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2440, + -140 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const raw = $input.first().json;\nconst task_id = (raw.data || '').replace(/\"/g, '').trim();\nconst meta = $('Prepara Upload').first().json;\nlet tag_ids = meta.tag_ids;\nif (typeof tag_ids === 'string') { try { tag_ids = JSON.parse(tag_ids); } catch(e) { tag_ids = []; } }\nreturn [{ json: { task_id, ...meta, tag_ids } }];" + } + }, + { + "id": "n21", + "name": "⏳ Attendi Paperless", + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 2660, + -140 + ], + "parameters": { + "amount": 20, + "unit": "seconds" + }, + "webhookId": "wait-pl-documento" + }, + { + "id": "n22", + "name": "Paperless - Stato Task", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2880, + -140 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/tasks/", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "task_id", + "value": "={{ $json.task_id }}" + } + ] + }, + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n23", + "name": "Estrai Document ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3100, + -140 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const tasks = $input.first().json;\nconst task = Array.isArray(tasks) ? tasks[0] : tasks;\nif (!task) throw new Error('Nessun task');\nconst isDup = task.status === 'FAILURE' && String(task.result || '').includes('duplicate');\nif (!isDup && task.status !== 'SUCCESS') throw new Error('Task ' + task.status + ': ' + (task.result || ''));\nconst doc_id = parseInt(String(task.related_document || '').replace(/\\D/g, ''), 10);\nif (!doc_id) throw new Error('Doc ID non trovato: ' + JSON.stringify(task));\nconst meta = $('Prepara Upload').first().json;\nlet tag_ids = meta.tag_ids;\nif (typeof tag_ids === 'string') { try { tag_ids = JSON.parse(tag_ids); } catch(e) { tag_ids = []; } }\nreturn [{ json: {\n doc_id, is_duplicate: isDup, title: meta.title,\n correspondent_id: meta.correspondent_id != null ? Number(meta.correspondent_id) : null,\n document_type_id: meta.document_type_id != null ? Number(meta.document_type_id) : null,\n storage_path_id: meta.storage_path_id != null ? Number(meta.storage_path_id) : null,\n tag_ids: Array.isArray(tag_ids) ? tag_ids.map(Number).filter(n => !isNaN(n)) : [],\n created_date: meta.created_date ? String(meta.created_date).substring(0, 10) : null,\n} }];" + } + }, + { + "id": "n24", + "name": "Paperless - Patch Metadati", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3320, + -140 + ], + "parameters": { + "method": "PATCH", + "url": "=https://docs.mt-home.uk/api/documents/{{ $json.doc_id }}/", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify(Object.fromEntries(Object.entries({correspondent:$json.correspondent_id,document_type:$json.document_type_id,storage_path:$json.storage_path_id,tags:$json.tag_ids&&$json.tag_ids.length>0?$json.tag_ids:undefined,created:$json.created_date||undefined}).filter(([_,v])=>v!==null&&v!==undefined))) }}", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "n25", + "name": "Telegram - Conferma Upload", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 3540, + -140 + ], + "parameters": { + "chatId": "-4814221197", + "text": "={{ '✅ *Documento archiviato su Paperless!*\\n\\n📄 *' + $('Prepara Upload').first().json.filename + '*\\n🗂 ' + $('Prepara Upload').first().json.title + '\\n' + ($('Estrai Document ID').first().json.is_duplicate ? '⚠️ _Duplicato — metadati aggiornati_\\n' : '') + '🔗 https://docs.mt-home.uk/documents/' + $json.doc_id + '/details/' }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "id": "n_fw_del", + "name": "FileWizard - Elimina File", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3760, + -140 + ], + "parameters": { + "method": "POST", + "url": "http://filewizard.home.svc.cluster.local:8000/settings/delete-files", + "options": {} + } + }, + { + "id": "n_fw_clr", + "name": "FileWizard - Pulisci Job", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3980, + -140 + ], + "parameters": { + "method": "POST", + "url": "http://filewizard.home.svc.cluster.local:8000/settings/clear-history", + "options": {} + } + } + ], + "connections": { + "Telegram Trigger": { + "main": [ + [ + { + "node": "Check Caption", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Caption": { + "main": [ + [ + { + "node": "Telegram - Scarica File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Telegram - Scarica File": { + "main": [ + [ + { + "node": "FileWizard - Avvia OCR", + "type": "main", + "index": 0 + } + ] + ] + }, + "FileWizard - Avvia OCR": { + "main": [ + [ + { + "node": "⏳ Attendi OCR", + "type": "main", + "index": 0 + } + ] + ] + }, + "⏳ Attendi OCR": { + "main": [ + [ + { + "node": "FileWizard - Stato OCR", + "type": "main", + "index": 0 + } + ] + ] + }, + "FileWizard - Stato OCR": { + "main": [ + [ + { + "node": "Paperless - Corrispondenti", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Corrispondenti": { + "main": [ + [ + { + "node": "Paperless - Tipi Doc", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Tipi Doc": { + "main": [ + [ + { + "node": "Paperless - Tag", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Tag": { + "main": [ + [ + { + "node": "Paperless - Percorsi", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Percorsi": { + "main": [ + [ + { + "node": "Build Prompt", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Prompt": { + "main": [ + [ + { + "node": "Ottieni Token Copilot", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ottieni Token Copilot": { + "main": [ + [ + { + "node": "GPT-4.1 - Inferisci Metadati", + "type": "main", + "index": 0 + } + ] + ] + }, + "GPT-4.1 - Inferisci Metadati": { + "main": [ + [ + { + "node": "Parse Risposta GPT", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Risposta GPT": { + "main": [ + [ + { + "node": "Skip?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Skip?": { + "main": [ + [ + { + "node": "Telegram - Documento Non Riconosciuto", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepara Upload", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepara Upload": { + "main": [ + [ + { + "node": "Paperless - Upload Documento", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Upload Documento": { + "main": [ + [ + { + "node": "Estrai Task ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Estrai Task ID": { + "main": [ + [ + { + "node": "⏳ Attendi Paperless", + "type": "main", + "index": 0 + } + ] + ] + }, + "⏳ Attendi Paperless": { + "main": [ + [ + { + "node": "Paperless - Stato Task", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Stato Task": { + "main": [ + [ + { + "node": "Estrai Document ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Estrai Document ID": { + "main": [ + [ + { + "node": "Paperless - Patch Metadati", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Patch Metadati": { + "main": [ + [ + { + "node": "Telegram - Conferma Upload", + "type": "main", + "index": 0 + } + ] + ] + }, + "Telegram - Conferma Upload": { + "main": [ + [ + { + "node": "FileWizard - Elimina File", + "type": "main", + "index": 0 + } + ] + ] + }, + "FileWizard - Elimina File": { + "main": [ + [ + { + "node": "FileWizard - Pulisci Job", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "ebc247d9-6391-45c1-94f8-f73ded65ce0b", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": true +} \ No newline at end of file diff --git a/workflows/o3uM1xDLTAKw4D6E.json b/workflows/o3uM1xDLTAKw4D6E.json new file mode 100644 index 0000000..c46a6cd --- /dev/null +++ b/workflows/o3uM1xDLTAKw4D6E.json @@ -0,0 +1,489 @@ +{ + "id": "o3uM1xDLTAKw4D6E", + "name": "🎬 Pompeo — Media Library Sync [Schedule]", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "weeks", + "triggerAtHour": 3 + } + ] + } + }, + "id": "b0ccc657-6f9e-43b7-b31e-e753e7c90c53", + "name": "⏰ Cron", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [ + 0, + 304 + ] + }, + { + "parameters": { + "url": "http://radarr.media.svc.cluster.local:7878/radarr/api/v3/movie?apikey=922d1405ab1147019d98a2997d941765", + "options": {} + }, + "id": "4b7a2a61-e7ca-41e4-b6fc-b1d112697306", + "name": "🎬 HTTP Radarr", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 224, + 192 + ] + }, + { + "parameters": { + "url": "http://sonarr.media.svc.cluster.local:8989/sonarr/api/v3/series?apikey=22140655993a4ff6bf12314813ec6982", + "options": {} + }, + "id": "a23916f3-e727-4553-bb62-ad3f2602c1f2", + "name": "📺 HTTP Sonarr", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 224, + 432 + ] + }, + { + "parameters": {}, + "id": "986ab3c1-3474-451a-bfa9-dfbeffbaca91", + "name": "🔀 Merge", + "type": "n8n-nodes-base.merge", + "typeVersion": 2, + "position": [ + 496, + 304 + ] + }, + { + "parameters": { + "jsCode": "const allItems = $input.all();\nconst movies = [];\nconst series = [];\n\nfor (const item of allItems) {\n const d = item.json;\n // d may be a full array response or a single object\n const src = Array.isArray(d) ? d : [d];\n for (const s of src) {\n if (s != null && s.tmdbId !== undefined) {\n movies.push({\n type: 'movie',\n title: s.title,\n year: s.year,\n genres: s.genres || [],\n status: s.hasFile ? 'available' : 'missing',\n source: 'radarr',\n source_id: s.tmdbId,\n monitored: s.monitored\n });\n } else if (s != null && s.tvdbId !== undefined) {\n series.push({\n type: 'series',\n title: s.title,\n year: s.year,\n genres: s.genres || [],\n status: s.monitored ? 'monitored' : 'unmonitored',\n source: 'sonarr',\n source_id: s.tvdbId,\n monitored: s.monitored\n });\n }\n }\n}\n\nconst items = [...movies, ...series];\n\nconst genreMap = {};\nfor (const item of items) {\n for (const genre of (item.genres || [])) {\n if (!genreMap[genre]) genreMap[genre] = [];\n genreMap[genre].push(item.title);\n }\n}\n\nlet library_summary = '';\nfor (const [genre, titles] of Object.entries(genreMap)) {\n library_summary += genre + ': ' + titles.join(', ') + '\\n';\n}\n\nreturn [{ json: { items, library_summary } }];" + }, + "id": "a4d4acff-c5b1-450f-8428-402ca6e5969c", + "name": "🔀 Merge Libreria", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 704, + 304 + ] + }, + { + "parameters": { + "url": "https://api.github.com/copilot_internal/v2/token", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "id": "57d1cfcb-a838-4382-99d9-4e2aff920511", + "name": "🔑 Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 928, + 304 + ], + "credentials": { + "httpHeaderAuth": { + "id": "vBwUxlzKrX3oDHyN", + "name": "GitHub Copilot OAuth Token" + } + } + }, + { + "parameters": { + "jsCode": "const mergeData = $('🔀 Merge Libreria').first().json;\nconst library_summary = mergeData.library_summary;\n\nconst prompt = 'Analizza questa libreria multimediale personale e restituisci un JSON con:\\n' +\n '{\\n' +\n ' \"top_genres\": [\"lista\", \"dei\", \"generi\", \"preferiti\"],\\n' +\n ' \"preferred_types\": \"movie|series|both\",\\n' +\n ' \"library_stats\": {\"total_movies\": N, \"total_series\": N, \"available_movies\": N},\\n' +\n ' \"taste_summary\": \"frase descrittiva del gusto cinematografico in italiano\",\\n' +\n ' \"notable_patterns\": [\"pattern1\", \"pattern2\"]\\n' +\n '}\\n\\n' +\n 'Libreria:\\n' + library_summary;\n\nreturn [{ json: { prompt } }];" + }, + "id": "ee1918af-97fd-4c45-8d22-9e9808a47842", + "name": "📝 Build Prompt LLM", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1136, + 304 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.githubcopilot.com/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('🔑 Ottieni Token Copilot').first().json.token }}" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Copilot-Integration-Id", + "value": "vscode-chat" + }, + { + "name": "Editor-Version", + "value": "vscode/1.85.0" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\": $('📝 Build Prompt LLM').first().json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":2048}) }}", + "options": {} + }, + "id": "709416ec-5b5a-4735-a985-9a98822b3910", + "name": "🤖 GPT-4.1 Analisi", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1328, + 304 + ] + }, + { + "parameters": { + "jsCode": "const content = $input.first().json.choices[0].message.content;\nreturn [{ json: JSON.parse(content) }];" + }, + "id": "e2888c8c-f2cf-4920-bab6-a32bf632b54f", + "name": "🔍 Parse LLM", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1504, + 304 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO memory_facts (user_id, source, category, subject, detail, expires_at, source_ref)\nVALUES (\n 'martin',\n 'media_library',\n 'media_preferences',\n 'Preferenze Media Martin',\n '{{ $json.detail_json }}'::jsonb,\n NOW() + INTERVAL '7 days',\n 'media_preferences_summary'\n)\nON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL\nDO UPDATE SET\n detail = EXCLUDED.detail,\n expires_at = EXCLUDED.expires_at,\n created_at = NOW();", + "options": {} + }, + "id": "04eb6255-231f-4ed6-bd35-45970280c1cd", + "name": "💾 PG — Upsert Preferenze", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2, + "position": [ + 1840, + 304 + ], + "credentials": { + "postgres": { + "id": "mRqzxhSboGscolqI", + "name": "Pompeo — PostgreSQL" + } + } + }, + { + "parameters": { + "jsCode": "const items = $('🔀 Merge Libreria').first().json.items;\nreturn items.map(item => ({ json: item }));" + }, + "id": "3fd8b3f4-f409-40e8-a747-2539248a959a", + "name": "📋 Espandi Items", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2000, + 304 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "09919f3a-5ccb-4003-96b0-f096f9c8e802", + "name": "🔁 Loop Items", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 2208, + 304 + ] + }, + { + "parameters": { + "method": "POST", + "url": "http://ollama.ai.svc.cluster.local:11434/api/embeddings", + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({\"model\":\"nomic-embed-text\",\"prompt\": $json.title + \" \" + ($json.year||\"\") + \" \" + ($json.genres||[]).join(\" \") + \" \" + ($json.type||\"\")}) }}", + "options": {} + }, + "id": "5b1aabe1-58fe-4290-baa6-8bb02ad144d8", + "name": "🔢 Ollama Embed", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2448, + 128 + ] + }, + { + "parameters": { + "method": "PUT", + "url": "http://qdrant.persistence.svc.cluster.local:6333/collections/media_preferences/points", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "api-key", + "value": "__Montecarlo00!" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ $json.qdrant_body }}", + "options": {} + }, + "id": "3b04cf16-e00b-4e3d-aef7-beea09153db0", + "name": "🗄️ Qdrant Upsert", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2688, + 320 + ] + }, + { + "parameters": { + "jsCode": "const parsed = $input.first().json;\n// Serialize to JSON and escape single quotes for SQL injection safety\nconst detailJson = JSON.stringify(parsed).split(\"'\").join(\"''\");\nreturn [{ json: { detail_json: detailJson } }];" + }, + "id": "bf16855c-423d-4eb7-b483-a54af827036f", + "name": "🔧 Prepara Detail", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1680, + 304 + ] + }, + { + "id": "e52627b8-ef32-47e1-94e4-8ecbdf2cc02b", + "name": "🔧 Prepara Qdrant", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2468, + 320 + ], + "parameters": { + "jsCode": "const item = $('🔁 Loop Items').item.json;\nconst embedding = $('🔢 Ollama Embed').item.json.embedding;\n\n// Use source_id as integer point ID (hash if needed)\nconst rawId = item.source_id;\nlet pointId = Math.abs(parseInt(rawId) || 0) % 2147483647;\nif (pointId === 0) pointId = Math.abs(String(rawId).split('').reduce((a,c) => a + c.charCodeAt(0), 0)) % 2147483647;\n\nconst payload = {\n points: [{\n id: pointId,\n vector: embedding,\n payload: {\n title: item.title || '',\n year: item.year || null,\n type: item.type || '',\n genres: item.genres || [],\n status: item.status || '',\n source: item.source || '',\n source_id: rawId,\n expires_at: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString()\n }\n }]\n};\n\nreturn [{ json: { qdrant_body: JSON.stringify(payload) } }];" + } + } + ], + "connections": { + "⏰ Cron": { + "main": [ + [ + { + "node": "🎬 HTTP Radarr", + "type": "main", + "index": 0 + }, + { + "node": "📺 HTTP Sonarr", + "type": "main", + "index": 0 + } + ] + ] + }, + "🎬 HTTP Radarr": { + "main": [ + [ + { + "node": "🔀 Merge", + "type": "main", + "index": 0 + } + ] + ] + }, + "📺 HTTP Sonarr": { + "main": [ + [ + { + "node": "🔀 Merge", + "type": "main", + "index": 1 + } + ] + ] + }, + "🔀 Merge": { + "main": [ + [ + { + "node": "🔀 Merge Libreria", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔀 Merge Libreria": { + "main": [ + [ + { + "node": "🔑 Ottieni Token Copilot", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔑 Ottieni Token Copilot": { + "main": [ + [ + { + "node": "📝 Build Prompt LLM", + "type": "main", + "index": 0 + } + ] + ] + }, + "📝 Build Prompt LLM": { + "main": [ + [ + { + "node": "🤖 GPT-4.1 Analisi", + "type": "main", + "index": 0 + } + ] + ] + }, + "🤖 GPT-4.1 Analisi": { + "main": [ + [ + { + "node": "🔍 Parse LLM", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔍 Parse LLM": { + "main": [ + [ + { + "node": "🔧 Prepara Detail", + "type": "main", + "index": 0 + } + ] + ] + }, + "💾 PG — Upsert Preferenze": { + "main": [ + [ + { + "node": "📋 Espandi Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "📋 Espandi Items": { + "main": [ + [ + { + "node": "🔁 Loop Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔁 Loop Items": { + "main": [ + [], + [ + { + "node": "🔢 Ollama Embed", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔢 Ollama Embed": { + "main": [ + [ + { + "node": "🔧 Prepara Qdrant", + "type": "main", + "index": 0 + } + ] + ] + }, + "🗄️ Qdrant Upsert": { + "main": [ + [ + { + "node": "🔁 Loop Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔧 Prepara Detail": { + "main": [ + [ + { + "node": "💾 PG — Upsert Preferenze", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔧 Prepara Qdrant": { + "main": [ + [ + { + "node": "🗄️ Qdrant Upsert", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "ef7c42b4-fddc-4e53-913a-47761d08f5a6", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file diff --git a/workflows/qtvB3r0cgejyCxUp.json b/workflows/qtvB3r0cgejyCxUp.json new file mode 100644 index 0000000..8f5d64a --- /dev/null +++ b/workflows/qtvB3r0cgejyCxUp.json @@ -0,0 +1,395 @@ +{ + "id": "qtvB3r0cgejyCxUp", + "name": "💰 Actual — Import Estratto Conto [Telegram]", + "nodes": [ + { + "parameters": { + "updates": [ + "message" + ], + "additionalFields": {} + }, + "id": "n_tg_trigger", + "name": "📩 Telegram Trigger", + "type": "n8n-nodes-base.telegramTrigger", + "typeVersion": 1.1, + "position": [ + 0, + 304 + ], + "webhookId": "actual-import-tg-webhook", + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": false, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "cond1", + "leftValue": "={{ ($json.message?.caption || '').toLowerCase() }}", + "rightValue": "estratto", + "operator": { + "type": "string", + "operation": "startsWith" + } + }, + { + "id": "cond2", + "leftValue": "={{ $json.message?.document?.file_id }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "exists", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "n_check", + "name": "🔍 È un Estratto?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 240, + 304 + ] + }, + { + "parameters": { + "url": "https://api.github.com/copilot_internal/v2/token", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "id": "n_token", + "name": "🔑 Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 464, + 160 + ], + "credentials": { + "httpHeaderAuth": { + "id": "vBwUxlzKrX3oDHyN", + "name": "GitHub Copilot OAuth Token" + } + } + }, + { + "parameters": { + "resource": "file", + "fileId": "={{ $json.file_id }}", + "additionalFields": {} + }, + "id": "n_scarica", + "name": "📥 Scarica File Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 912, + 160 + ], + "webhookId": "e983ec4e-194c-44bf-9a2a-c78a3280fcf5", + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "jsCode": "const chatId = $('📋 Prepara File ID').first().json.chat_id || -4814221197;\n\n// Use getBinaryDataBuffer() — works with both memory and filesystem binary modes\nconst buf = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst csvText = buf.toString('utf-8').replace(/\\r/g, '');\n\nfunction parseLine(line) {\n const parts = []; let cur = '', inQ = false;\n for (const ch of line) {\n if (ch === '\"') { inQ = !inQ; }\n else if (ch === ';' && !inQ) { parts.push(cur.trim()); cur = ''; }\n else { cur += ch; }\n }\n parts.push(cur.trim());\n return parts;\n}\n\nfunction simpleHash(str) {\n let h = 0;\n for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;\n return Math.abs(h).toString(16).padStart(8, '0');\n}\n\nconst lines = csvText.split('\\n').map(l => l.trim()).filter(Boolean);\nconst dataLines = lines.filter(line => {\n const upper = line.toUpperCase();\n if (upper.startsWith('\"DATA\"') || upper.startsWith('DATA;')) return false;\n if (upper.includes('SALDO FINALE') || upper.includes('SALDO INIZIALE')) return false;\n return true;\n});\n\nconst transactions = [];\nfor (const line of dataLines) {\n if (!line || line.length < 10) continue;\n const cols = parseLine(line);\n if (cols.length < 5) continue;\n const [dataStr, , causale, descrizione, importoStr] = cols;\n if (!dataStr || !importoStr) continue;\n const parts = dataStr.split('/');\n if (parts.length !== 3) continue;\n const [day, month, year] = parts;\n if (!year || year.length !== 4) continue;\n const date = `${year}-${month.padStart(2,'0')}-${day.padStart(2,'0')}`;\n const cleanAmt = importoStr.replace(/[\"+\\s]/g, '');\n const amount = Math.round(parseFloat(cleanAmt) * 100);\n if (isNaN(amount)) continue;\n const idMatch = (descrizione || '').match(/Id\\.\\s*(\\d+)/);\n const importId = idMatch\n ? `banca-sella-${idMatch[1]}`\n : `banca-sella-${date}-${simpleHash(`${date}|${amount}|${descrizione || causale}`)}`;\n transactions.push({ date, amount, causale: causale || '', descrizione: descrizione || '', importId });\n}\n\nconst dates = transactions.map(t => t.date).sort();\nconst minDate = dates[0] || new Date().toISOString().split('T')[0];\nreturn [{ json: { transactions, min_date: minDate, total_parsed: transactions.length, chat_id: chatId } }];\n" + }, + "id": "n_parse", + "name": "🔄 Parse CSV", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1152, + 160 + ] + }, + { + "parameters": { + "jsCode": "const BUDGET_ID = '56c4e1c8-634b-4ebc-8fee-e7f0fe2b4e52';\nconst API_KEY = 'jcGZGgiSTX3ptQr+m3Z61VVzAjFNxeqcJjM266I+S2Vd8QS4wX';\nconst BASE_URL = 'http://actual.home.svc.cluster.local:5007';\nconst H = { 'x-api-key': API_KEY, 'accept': 'application/json' };\n\nconst { transactions, min_date, total_parsed, chat_id } = $input.first().json;\n\n// Retry helper: actual-http-api may need to sync budget on first call\nasync function fetchWithRetry(url, maxAttempts = 3, delayMs = 2000) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const res = await this.helpers.httpRequest({ method: 'GET', url, headers: H });\n return res;\n } catch(e) {\n if (i < maxAttempts - 1) {\n await new Promise(r => setTimeout(r, delayMs));\n } else {\n throw e;\n }\n }\n }\n}\n\n// Sequential calls (not parallel) to avoid hitting actual-http-api concurrently on first sync\nconst payeesRes = await fetchWithRetry.call(this, `${BASE_URL}/v1/budgets/${BUDGET_ID}/payees`);\nconst categoriesRes = await fetchWithRetry.call(this, `${BASE_URL}/v1/budgets/${BUDGET_ID}/categories`);\n\nreturn [{ json: {\n transactions,\n total_parsed,\n existing_payees: payeesRes.data || [],\n existing_categories: categoriesRes.data || [],\n chat_id\n}}];\n" + }, + "id": "n_fetch", + "name": "📊 Fetch Dati Actual", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1392, + 160 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "cond_empty", + "leftValue": "={{ $json.total_parsed }}", + "rightValue": 0, + "operator": { + "type": "number", + "operation": "gt" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "n_nonuove", + "name": "🚫 Nuove Transazioni?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 1632, + 160 + ] + }, + { + "parameters": { + "chatId": "={{ $json.chat_id || '-4814221197' }}", + "text": "={{ '✅ Nessuna nuova transazione da importare — tutte le ' + $json.total_parsed + ' transazioni risultano già presenti su Actual Budget.' }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "n_tg_niente", + "name": "📱 Nessuna Tx Nuova", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 1920, + 384 + ], + "webhookId": "49aca71e-f36b-4c65-9502-58b07f5f7e9e", + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "chatId": "={{ $json.chat_id || '-4814221197' }}", + "text": "={{ $json.message }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "id": "n_report", + "name": "📱 Report Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 2224, + -48 + ], + "webhookId": "872c3805-f7d6-4342-987b-e3d641af693c", + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "jsCode": "\n// Mark Google Task \"Actual - Estratto conto\" in list \"Finanze\" as completed\n// Uses Calendar OAuth credential via predefinedCredentialType\n// Non-blocking: errors are logged but don't fail the workflow\nconst TASK_NAME = 'Actual - Estratto conto';\nconst TASKLIST_NAME = 'Finanze';\n\ntry {\n // Get task lists\n const listsRes = await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'GET',\n url: 'https://tasks.googleapis.com/tasks/v1/users/@me/lists',\n json: true\n }\n );\n \n const taskList = (listsRes.items || []).find(l => l.title === TASKLIST_NAME);\n if (!taskList) {\n console.log(`Task list \"${TASKLIST_NAME}\" not found`);\n return $input.all();\n }\n \n // Get tasks from the list\n const tasksRes = await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'GET',\n url: `https://tasks.googleapis.com/tasks/v1/lists/${taskList.id}/tasks?showCompleted=false&showHidden=false`,\n json: true\n }\n );\n \n const task = (tasksRes.items || []).find(t => t.title === TASK_NAME);\n if (!task) {\n console.log(`Task \"${TASK_NAME}\" not found or already completed`);\n return $input.all();\n }\n \n // Mark as completed\n await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'PATCH',\n url: `https://tasks.googleapis.com/tasks/v1/lists/${taskList.id}/tasks/${task.id}`,\n body: { status: 'completed' },\n json: true\n }\n );\n \n console.log(`Task \"${TASK_NAME}\" marked as completed`);\n} catch(e) {\n // Non-blocking - just log the error\n console.error('Google Tasks error (non-blocking):', e.message);\n}\n\nreturn $input.all();\n" + }, + "id": "n_mark_task", + "name": "✅ Marca Task Completato", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2528, + -48 + ] + }, + { + "parameters": { + "jsCode": "const tg = $('📩 Telegram Trigger').first().json;\nreturn [{ json: {\n file_id: tg.message.document.file_id,\n chat_id: tg.message.chat.id,\n caption: tg.message.caption || ''\n}}];\n" + }, + "id": "n_prepara_file", + "name": "📋 Prepara File ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 688, + 160 + ] + }, + { + "parameters": { + "jsCode": "const BUDGET_ID = '56c4e1c8-634b-4ebc-8fee-e7f0fe2b4e52';\nconst ACCOUNT_ID = '7558ab93-e795-4b3b-9766-5a6d47d99f92';\nconst API_KEY = 'jcGZGgiSTX3ptQr+m3Z61VVzAjFNxeqcJjM266I+S2Vd8QS4wX';\nconst BASE_URL = 'http://actual.home.svc.cluster.local:5007';\nconst EXPENSE_GROUP = 'fc3825fd-b982-4b72-b768-5b30844cf832';\nconst INCOME_GROUP = '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00';\nconst BATCH_SIZE = 30;\n\nconst H_JSON = { 'x-api-key': API_KEY, 'content-type': 'application/json', 'accept': 'application/json' };\nconst H_GET = { 'x-api-key': API_KEY, 'accept': 'application/json' };\n\nconst { transactions, total_parsed, chat_id } = $input.first().json;\nconst copilotToken = $('🔑 Ottieni Token Copilot').first().json.token;\n\n// ── 1. Fetch payees & categories ──────────────────────────────────────────\nasync function apiGet(path) {\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n return await this.helpers.httpRequest({ method: 'GET', url: `${BASE_URL}${path}`, headers: H_GET });\n } catch(e) {\n if (attempt < 2) await new Promise(r => setTimeout(r, 2000));\n else throw e;\n }\n }\n}\n\nconst [payeesRes, catsRes] = await Promise.all([\n apiGet.call(this, `/v1/budgets/${BUDGET_ID}/payees`),\n apiGet.call(this, `/v1/budgets/${BUDGET_ID}/categories`),\n]);\n\nlet existingPayees = payeesRes.data || [];\nlet existingCategories = catsRes.data || [];\n\nconst payeeNameMap = () => Object.fromEntries(existingPayees.map(p => [p.name.toLowerCase(), p.id]));\nconst catNameMap = () => Object.fromEntries(existingCategories.map(c => [c.name.toLowerCase(), c.id]));\n\n// ── 2. Loop batches ───────────────────────────────────────────────────────\nlet totalAdded = 0, totalUpdated = 0;\nconst allNewPayeeNames = [], allNewCatNames = [];\n\nfor (let batchStart = 0; batchStart < transactions.length; batchStart += BATCH_SIZE) {\n const batch = transactions.slice(batchStart, batchStart + BATCH_SIZE);\n const batchIndex = Math.floor(batchStart / BATCH_SIZE);\n\n // Build GPT prompt\n const payeeList = existingPayees.map(p => `${p.id}|${p.name}`).join('\\n');\n const catList = existingCategories.map(c => `${c.id}|${c.name}|${c.is_income ? 'entrata' : 'uscita'}`).join('\\n');\n const txList = batch.map((t, i) =>\n `${i+1}. ${t.date} | ${(t.amount/100).toFixed(2)}€ | ${t.causale} | ${(t.descrizione||'').substring(0,120)}`\n ).join('\\n');\n\n const userPrompt = `Classifica le seguenti transazioni bancarie per il budget personale di Martin.\n\nPAYEE ESISTENTI (id|nome):\n${payeeList}\n\nCATEGORIE ESISTENTI (id|nome|tipo):\n${catList}\n\nTRANSAZIONI (indice. data | importo | causale | descrizione):\n${txList}\n\nRispondi SOLO con JSON valido in questo formato:\n{\n \"new_payees\": [\"Nome Payee da creare\"],\n \"new_categories\": [{\"name\": \"Nome Categoria\", \"is_income\": false}],\n \"transactions\": [\n {\"index\": 1, \"payee_id\": \"uuid-esistente-o-null\", \"payee_name\": \"nome pulito\", \"category_id\": \"uuid-esistente-o-null\", \"category_name\": \"nome categoria\", \"notes\": \"\"}\n ]\n}\n\nRegole:\n- payee_id: UUID dalla lista se corrisponde, altrimenti null e aggiungi a new_payees\n- category_id: UUID dalla lista se corrisponde, altrimenti null e aggiungi a new_categories\n- payee_name e category_name SEMPRE valorizzati\n- Importi negativi = uscite (categorie tipo \"uscita\")\n- Importi positivi = entrate (categorie tipo \"entrata\")\n- PAGAMENTO MASTERCARD: classifica per contenuto descrizione\n- PRELIEVO ATM/CASSA → categoria Prelievo\n- ADDEBITO SDD/utenze → categoria Bollette\n- Nomi payee leggibili (Glovo non GLOVO ITALIA SRL)\n- Consistenza: stesso esercente = stesso payee sempre`;\n\n // Call GPT\n let gptResult = { new_payees: [], new_categories: [], transactions: [] };\n try {\n const gptRes = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://api.githubcopilot.com/chat/completions',\n headers: {\n 'Authorization': `Bearer ${copilotToken}`,\n 'editor-version': 'vscode/1.95.3',\n 'Copilot-Integration-Id': 'vscode-chat',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model: 'gpt-4.1',\n messages: [\n { role: 'system', content: 'Sei un assistente per finanze personali. Rispondi SEMPRE con JSON valido e niente altro.' },\n { role: 'user', content: userPrompt }\n ],\n temperature: 0.1,\n response_format: { type: 'json_object' }\n })\n });\n const content = gptRes.choices?.[0]?.message?.content || '{}';\n gptResult = JSON.parse(content);\n } catch(e) {\n // If GPT fails for this batch, skip it (don't fail the whole workflow)\n console.error(`Batch ${batchIndex} GPT error: ${e.message}`);\n continue;\n }\n\n const { new_payees = [], new_categories = [], transactions: classified = [] } = gptResult;\n\n // Create missing payees\n const pMap = payeeNameMap();\n const createdPayees = {};\n for (const payeeName of new_payees) {\n const existId = pMap[payeeName.toLowerCase()];\n if (existId) { createdPayees[payeeName] = existId; continue; }\n try {\n const res = await this.helpers.httpRequest({\n method: 'POST', url: `${BASE_URL}/v1/budgets/${BUDGET_ID}/payees`, headers: H_JSON,\n body: JSON.stringify({ payee: { name: payeeName } })\n });\n const newId = res.data;\n createdPayees[payeeName] = newId;\n existingPayees.push({ id: newId, name: payeeName, is_income: false });\n allNewPayeeNames.push(payeeName);\n } catch(e) { createdPayees[payeeName] = pMap[payeeName.toLowerCase()] || null; }\n }\n\n // Create missing categories\n const cMap = catNameMap();\n const createdCategories = {};\n for (const cat of new_categories) {\n const existId = cMap[cat.name.toLowerCase()];\n if (existId) { createdCategories[cat.name] = existId; continue; }\n const groupId = cat.is_income ? INCOME_GROUP : EXPENSE_GROUP;\n try {\n const res = await this.helpers.httpRequest({\n method: 'POST', url: `${BASE_URL}/v1/budgets/${BUDGET_ID}/categories`, headers: H_JSON,\n body: JSON.stringify({ category: { name: cat.name, group_id: groupId } })\n });\n const newId = res.data;\n createdCategories[cat.name] = newId;\n existingCategories.push({ id: newId, name: cat.name, is_income: cat.is_income || false });\n allNewCatNames.push(cat.name);\n } catch(e) { createdCategories[cat.name] = cMap[cat.name.toLowerCase()] || null; }\n }\n\n // Map classified transactions\n const freshPMap = payeeNameMap();\n const freshCMap = catNameMap();\n const toImport = classified.map((c, i) => {\n const tx = batch[c.index - 1] ?? batch[i];\n if (!tx) return null;\n const payeeId = (c.payee_id && existingPayees.find(p => p.id === c.payee_id))\n ? c.payee_id\n : (createdPayees[c.payee_name] || freshPMap[c.payee_name?.toLowerCase()]);\n const catId = (c.category_id && existingCategories.find(cat => cat.id === c.category_id))\n ? c.category_id\n : (createdCategories[c.category_name] || freshCMap[c.category_name?.toLowerCase()]);\n return {\n account: ACCOUNT_ID, date: tx.date, amount: tx.amount,\n ...(payeeId ? { payee: payeeId } : { payee_name: c.payee_name || tx.descrizione?.substring(0,50) || 'Sconosciuto' }),\n ...(catId ? { category: catId } : {}),\n notes: c.notes || '', imported_id: tx.importId, cleared: true\n };\n }).filter(Boolean);\n\n if (toImport.length > 0) {\n try {\n const importRes = await this.helpers.httpRequest({\n method: 'POST',\n url: `${BASE_URL}/v1/budgets/${BUDGET_ID}/accounts/${ACCOUNT_ID}/transactions/import`,\n headers: H_JSON,\n body: JSON.stringify({ transactions: toImport })\n });\n totalAdded += importRes.data?.added?.length || 0;\n totalUpdated += importRes.data?.updated?.length || 0;\n } catch(e) {\n console.error(`Batch ${batchIndex} import error: ${e.message}`);\n }\n }\n}\n\n// ── 3. Build report message ───────────────────────────────────────────────\nlet msg = '💰 *Estratto Conto Importato*\\n\\n';\nmsg += `✅ Nuove: *${totalAdded}* transazioni\\n`;\nif (totalUpdated > 0) msg += `⏭️ Già presenti: *${totalUpdated}*\\n`;\nmsg += `📊 Totale CSV: *${total_parsed || 0}*`;\n\nreturn [{ json: { message: msg, chat_id: chat_id || -4814221197, total_parsed, totalAdded } }];" + }, + "id": "n_classifica", + "name": "🧠 Classifica e Importa", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1920, + -48 + ] + } + ], + "connections": { + "📩 Telegram Trigger": { + "main": [ + [ + { + "node": "🔍 È un Estratto?", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔍 È un Estratto?": { + "main": [ + [ + { + "node": "🔑 Ottieni Token Copilot", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "🔑 Ottieni Token Copilot": { + "main": [ + [ + { + "node": "📋 Prepara File ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "📥 Scarica File Telegram": { + "main": [ + [ + { + "node": "🔄 Parse CSV", + "type": "main", + "index": 0 + } + ] + ] + }, + "🔄 Parse CSV": { + "main": [ + [ + { + "node": "📊 Fetch Dati Actual", + "type": "main", + "index": 0 + } + ] + ] + }, + "📊 Fetch Dati Actual": { + "main": [ + [ + { + "node": "🚫 Nuove Transazioni?", + "type": "main", + "index": 0 + } + ] + ] + }, + "🚫 Nuove Transazioni?": { + "main": [ + [ + { + "node": "🧠 Classifica e Importa", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "📱 Nessuna Tx Nuova", + "type": "main", + "index": 0 + } + ] + ] + }, + "📱 Report Telegram": { + "main": [ + [ + { + "node": "✅ Marca Task Completato", + "type": "main", + "index": 0 + } + ] + ] + }, + "📋 Prepara File ID": { + "main": [ + [ + { + "node": "📥 Scarica File Telegram", + "type": "main", + "index": 0 + } + ] + ] + }, + "🧠 Classifica e Importa": { + "main": [ + [ + { + "node": "📱 Report Telegram", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "43101c9f-8626-4d1c-8c48-2f2f467a4e17", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file diff --git a/workflows/vbzQ3fgUalOPdcOq.json b/workflows/vbzQ3fgUalOPdcOq.json new file mode 100644 index 0000000..9e92aa7 --- /dev/null +++ b/workflows/vbzQ3fgUalOPdcOq.json @@ -0,0 +1,734 @@ +{ + "id": "vbzQ3fgUalOPdcOq", + "name": "📄 Paperless — Bolletta Allegata", + "nodes": [ + { + "id": "wb-bolletta", + "name": "📎 Webhook Bolletta", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + -2200, + 0 + ], + "webhookId": "paperless-bolletta-001", + "parameters": { + "path": "paperless-bolletta", + "httpMethod": "POST", + "responseMode": "onReceived", + "options": {} + } + }, + { + "id": "gmail-get-msg", + "name": "Gmail - Leggi Messaggio", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -1900, + 0 + ], + "parameters": { + "method": "GET", + "url": "=https://gmail.googleapis.com/gmail/v1/users/me/messages/{{ $json.body.email_id }}?format=full", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "gmailOAuth2", + "options": {} + }, + "credentials": { + "gmailOAuth2": { + "id": "qvOikS6IF0H5khr8", + "name": "Gmail account" + } + } + }, + { + "id": "find-pdf", + "name": "Trova Allegato PDF", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1600, + 0 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const msg = $input.first().json;\n// La risposta Gmail API ha payload.parts con le parti del messaggio\nfunction findAttachments(parts, results) {\n if (!parts) return;\n for (const part of parts) {\n if (part.parts) findAttachments(part.parts, results);\n if (part.body && part.body.attachmentId) {\n results.push({\n attachment_id: part.body.attachmentId,\n filename: part.filename || 'allegato.pdf',\n mime_type: part.mimeType || 'application/octet-stream',\n size: part.body.size || 0\n });\n }\n }\n}\n\nconst parts = msg.payload && msg.payload.parts ? msg.payload.parts : [];\nconst attachments = [];\nfindAttachments(parts, attachments);\n\nconst pdf = attachments.find(a =>\n a.mime_type === 'application/pdf' ||\n a.filename.toLowerCase().endsWith('.pdf')\n) || attachments[0];\n\nif (!pdf) {\n throw new Error('Nessun allegato trovato nel messaggio. Parts: ' + JSON.stringify(parts).substring(0,200));\n}\n\nreturn [{json: {\n email_id: msg.id,\n thread_id: msg.threadId || msg.id,\n attachment_id: pdf.attachment_id,\n filename: pdf.filename,\n mime_type: pdf.mime_type,\n from: msg.payload?.headers?.find(h=>h.name==='From')?.value || '',\n subject: msg.payload?.headers?.find(h=>h.name==='Subject')?.value || '',\n}}];" + } + }, + { + "id": "dl-att", + "name": "Gmail - Download Allegato", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -1300, + 0 + ], + "parameters": { + "method": "GET", + "url": "=https://gmail.googleapis.com/gmail/v1/users/me/messages/{{ $json.email_id }}/attachments/{{ $json.attachment_id }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "gmailOAuth2", + "options": {} + }, + "credentials": { + "gmailOAuth2": { + "id": "qvOikS6IF0H5khr8", + "name": "Gmail account" + } + } + }, + { + "id": "decode-att", + "name": "Decodifica Allegato", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1000, + 0 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// La Gmail API restituisce il PDF in base64url (non standard base64)\nconst data = $input.first().json.data;\nif (!data) throw new Error('Nessun dato binario nella risposta attachment');\n\n// Converti base64url → base64 standard\nconst base64 = data.replace(/-/g, '+').replace(/_/g, '/');\nconst buffer = Buffer.from(base64, 'base64');\n\n// Prendi filename dal nodo precedente (Trova Allegato PDF)\nconst filename = $('Trova Allegato PDF').first().json.filename || 'bolletta.pdf';\nconst mimeType = $('Trova Allegato PDF').first().json.mime_type || 'application/pdf';\n\nconst binaryData = await this.helpers.prepareBinaryData(buffer, filename, mimeType);\nreturn [{json: {\n email_id: $('Trova Allegato PDF').first().json.email_id,\n filename,\n from: $('Trova Allegato PDF').first().json.from,\n subject: $('Trova Allegato PDF').first().json.subject,\n}, binary: { attachment: binaryData }}];" + } + }, + { + "id": "pl-corresp", + "name": "Paperless - Corrispondenti", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -700, + -200 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/correspondents/?page_size=100", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "pl-doctypes", + "name": "Paperless - Tipi Doc", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -400, + -200 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/document_types/?page_size=100", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "pl-tags", + "name": "Paperless - Tag", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -100, + -200 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/tags/?page_size=100", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "pl-paths", + "name": "Paperless - Percorsi", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 200, + -200 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/storage_paths/?page_size=100", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "pl-search", + "name": "Paperless - Cerca Simili", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 500, + -200 + ], + "parameters": { + "method": "GET", + "url": "=https://docs.mt-home.uk/api/documents/?page_size=5&ordering=-created&search={{ encodeURIComponent($('Trova Allegato PDF').first().json.from.replace(/.*<(.*)>.*/, '$1').split('@')[0]) }}", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "build-prompt", + "name": "Build Prompt Inferenza", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 800, + -200 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const meta = $input.first().json;\nconst emailMeta = $('Decodifica Allegato').first().json;\n\n// Fetch all Paperless metadata from previous nodes\nconst correspondents = ($('Paperless - Corrispondenti').first().json.results || [])\n .map(c => `[${c.id}] ${c.name} (documenti: ${c.document_count||0})`).join('\\n');\n\nconst docTypes = ($('Paperless - Tipi Doc').first().json.results || [])\n .map(d => `[${d.id}] ${d.name}`).join('\\n');\n\nconst tags = ($('Paperless - Tag').first().json.results || [])\n .map(t => `[${t.id}] ${t.name}`).join('\\n');\n\nconst storagePaths = ($('Paperless - Percorsi').first().json.results || [])\n .map(s => `[${s.id}] ${s.name}`).join('\\n');\n\nconst similarDocs = ($('Paperless - Cerca Simili').first().json.results || []).slice(0, 5)\n .map(d => `- \"${d.title}\" → corrispondente: ${d.correspondent||'?'}, tipo: ${d.document_type||'?'}, tags: ${(d.tags||[]).join(',')}, path: ${d.storage_path||'?'}`)\n .join('\\n');\n\nconst prompt = `Sei un assistente che gestisce l'archivio documenti di Martin su Paperless-NGX.\nAnalizza questa email con allegato e scegli i metadati corretti per archiviare il documento.\n\nEMAIL:\n- Da: ${emailMeta.from}\n- Oggetto: ${emailMeta.subject}\n- Nome file allegato: ${emailMeta.filename}\n\nCORRISPONDENTI DISPONIBILI (usa l'ID numerico):\n${correspondents}\n\nTIPI DOCUMENTO DISPONIBILI:\n${docTypes}\n\nTAG DISPONIBILI:\n${tags}\n\nPERCORSI DI ARCHIVIAZIONE DISPONIBILI:\n${storagePaths}\n\nDOCUMENTI SIMILI GIÀ ARCHIVIATI (per riferimento):\n${similarDocs || 'Nessuno trovato'}\n\nISTRUZIONI:\n1. Identifica il corrispondente più adatto in base al mittente/oggetto. Se non esiste, metti null.\n2. Scegli il tipo documento più appropriato (Bolletta, Fattura, Ricevuta, etc.)\n3. Scegli i tag appropriati (puoi sceglierne più di uno)\n4. Scegli il percorso di archiviazione più adatto\n5. Genera un titolo breve e descrittivo in italiano (es: \"E.ON Bolletta Energia Mar 2026\")\n6. Inferisci la data del documento se possibile dall'oggetto/mittente (formato YYYY-MM-DD)\n\nRispondi SOLO con JSON valido:\n{\n \"correspondent_id\": 44,\n \"document_type_id\": 2,\n \"tag_ids\": [3],\n \"storage_path_id\": 2,\n \"title\": \"E.ON Bolletta Energia Marzo 2026\",\n \"created_date\": \"2026-03-01\",\n \"confidence_note\": \"Breve nota sul ragionamento\"\n}`;\n\nreturn [{ json: { prompt, ...meta } }];" + } + }, + { + "id": "copilot-token", + "name": "Ottieni Token Copilot", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1100, + -200 + ], + "parameters": { + "method": "GET", + "url": "https://api.github.com/copilot_internal/v2/token", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "vBwUxlzKrX3oDHyN", + "name": "GitHub Copilot OAuth Token" + } + } + }, + { + "id": "gpt41-infer", + "name": "GPT-4.1 - Inferisci Metadati", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1400, + -200 + ], + "parameters": { + "method": "POST", + "url": "https://api.githubcopilot.com/chat/completions", + "authentication": "none", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "={{ 'Bearer ' + $('Ottieni Token Copilot').first().json.token }}" + }, + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "Copilot-Integration-Id", + "value": "vscode-chat" + }, + { + "name": "Editor-Version", + "value": "vscode/1.85.0" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "application/json", + "body": "={{ JSON.stringify({\"model\":\"gpt-4.1\",\"messages\":[{\"role\":\"system\",\"content\":\"Rispondi SOLO con JSON valido.\"},{\"role\":\"user\",\"content\": $('Build Prompt Inferenza').first().json.prompt}],\"response_format\":{\"type\":\"json_object\"},\"max_tokens\":1024}) }}", + "options": {} + } + }, + { + "id": "parse-gpt", + "name": "Parse Risposta GPT", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1700, + -200 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const raw = $input.first().json;\nconst content = (raw.choices && raw.choices[0] && raw.choices[0].message)\n ? raw.choices[0].message.content : (raw.text || '');\n\nconst cleaned = content.replace(/```json\\n?/g,'').replace(/```\\n?/g,'').trim();\n\nlet parsed;\ntry {\n parsed = JSON.parse(cleaned);\n} catch(e) {\n const m = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (m) parsed = JSON.parse(m[0]);\n else throw new Error('Cannot parse GPT response: ' + content.substring(0,300));\n}\n\n// Merge with attachment binary from previous node\nconst attItem = $('Decodifica Allegato').first();\n\nreturn [{\n json: {\n correspondent_id: parsed.correspondent_id || null,\n document_type_id: parsed.document_type_id || null,\n tag_ids: parsed.tag_ids || [],\n storage_path_id: parsed.storage_path_id || null,\n title: parsed.title || 'Documento',\n created_date: parsed.created_date || null,\n confidence_note: parsed.confidence_note || '',\n filename: attItem.json.filename,\n from: attItem.json.from,\n subject: attItem.json.subject,\n },\n binary: attItem.binary\n}];" + } + }, + { + "id": "prepara-upload", + "name": "Prepara Upload", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + -200 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const meta = $('Parse Risposta GPT').first().json;\nconst decodeNode = $('Decodifica Allegato').first();\nconst emailMeta = decodeNode.json;\n\n// Titolo = filename allegato senza estensione (es: \"10968276\")\nconst filename = emailMeta.filename || 'bolletta.pdf';\nconst title = filename.replace(/\\.[^/.]+$/, '');\n\n// Data = data ricezione email (internalDate è in millisecondi)\nconst internalDate = $('Gmail - Leggi Messaggio').first().json.internalDate;\nconst receivedISO = internalDate\n ? new Date(parseInt(internalDate)).toISOString()\n : null;\n\n// Parse tag_ids\nlet tag_ids = meta.tag_ids;\nif (typeof tag_ids === 'string') {\n try { tag_ids = JSON.parse(tag_ids); } catch(e) { tag_ids = []; }\n}\n\nconst result = {\n title,\n filename,\n correspondent_id: meta.correspondent_id != null ? Number(meta.correspondent_id) : null,\n document_type_id: meta.document_type_id != null ? Number(meta.document_type_id) : null,\n storage_path_id: meta.storage_path_id != null ? Number(meta.storage_path_id) : null,\n tag_ids: Array.isArray(tag_ids) ? tag_ids.map(Number).filter(n => !isNaN(n)) : [],\n created_date: receivedISO,\n};\n\nreturn [{ json: result, binary: decodeNode.binary }];" + } + }, + { + "id": "pl-upload", + "name": "Paperless - Upload Documento", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2000, + -200 + ], + "parameters": { + "method": "POST", + "url": "https://docs.mt-home.uk/api/documents/post_document/", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "parameterType": "formBinaryData", + "name": "document", + "inputDataFieldName": "attachment" + }, + { + "name": "title", + "value": "={{ $json.title }}" + } + ] + }, + "options": { + "response": { + "response": { + "responseFormat": "text" + } + } + } + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "tg-confirm", + "name": "Telegram - Conferma Upload", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 2300, + -200 + ], + "parameters": { + "chatId": "-4814221197", + "text": "={{ '✅ *Documento archiviato su Paperless!*\\n\\n📄 *' + $('Prepara Upload').first().json.filename + '*\\n🗂 ' + $('Prepara Upload').first().json.title + '\\n' + ($('Estrai Document ID').first().json.is_duplicate ? '⚠️ _Duplicato — metadati aggiornati_\\n' : '') + '🔗 https://docs.mt-home.uk/documents/' + $json.id + '/details/' }}", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + }, + { + "id": "estrai-task-id", + "name": "Estrai Task ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2220, + -200 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "// Paperless post_document risponde con {data: \"uuid\"} (responseFormat: text → n8n wrappa in {data:...})\nconst raw = $input.first().json;\nconst task_id = raw.data || raw.body || String(raw);\nconst meta = $('Prepara Upload').first().json;\n\n// Parse tag_ids se arriva come stringa\nlet tag_ids = meta.tag_ids;\nif (typeof tag_ids === 'string') {\n try { tag_ids = JSON.parse(tag_ids); } catch(e) { tag_ids = []; }\n}\n\nreturn [{ json: { \n task_id: task_id.replace(/\"/g,'').trim(),\n ...meta,\n tag_ids\n}}];" + } + }, + { + "id": "wait-paperless", + "name": "⏳ Attendi Paperless", + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [ + 2440, + -200 + ], + "parameters": { + "amount": 20, + "unit": "seconds" + }, + "webhookId": "wait-paperless-bolletta" + }, + { + "id": "get-task-status", + "name": "Paperless - Stato Task", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2660, + -200 + ], + "parameters": { + "method": "GET", + "url": "https://docs.mt-home.uk/api/tasks/", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "task_id", + "value": "={{ $json.task_id }}" + } + ] + }, + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + }, + { + "id": "estrai-doc-id", + "name": "Estrai Document ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2880, + -200 + ], + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "const tasks = $input.first().json;\nconst taskList = Array.isArray(tasks) ? tasks : [tasks];\nconst task = taskList[0];\nif (!task) throw new Error('Nessun task trovato');\n\nconst isDuplicate = task.status === 'FAILURE' && String(task.result || '').includes('duplicate');\nconst isSuccess = task.status === 'SUCCESS';\nif (!isSuccess && !isDuplicate) {\n throw new Error(`Task ${task.status}: ${task.result || task.task_error || ''}`);\n}\n\nconst doc_id = parseInt(String(task.related_document || '').replace(/\\D/g,''), 10);\nif (!doc_id) throw new Error('Document ID non trovato: ' + JSON.stringify(task));\n\nconst meta = $('Prepara Upload').first().json;\n\nlet tag_ids = meta.tag_ids;\nif (typeof tag_ids === 'string') {\n try { tag_ids = JSON.parse(tag_ids); } catch(e) { tag_ids = []; }\n}\n\n// created_date è già ISO completo (es: \"2026-03-18T21:26:51.000Z\"), prendi solo YYYY-MM-DD\nconst created_date = meta.created_date\n ? String(meta.created_date).substring(0, 10)\n : null;\n\nreturn [{ json: {\n doc_id,\n is_duplicate: isDuplicate,\n title: meta.title,\n correspondent_id: meta.correspondent_id != null ? Number(meta.correspondent_id) : null,\n document_type_id: meta.document_type_id != null ? Number(meta.document_type_id) : null,\n storage_path_id: meta.storage_path_id != null ? Number(meta.storage_path_id) : null,\n tag_ids: Array.isArray(tag_ids) ? tag_ids.map(Number).filter(n => !isNaN(n)) : [],\n created_date,\n}}];" + } + }, + { + "id": "patch-doc-meta", + "name": "Paperless - Patch Metadati", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3100, + -200 + ], + "parameters": { + "method": "PATCH", + "url": "=https://docs.mt-home.uk/api/documents/{{ $json.doc_id }}/", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ \n JSON.stringify(\n Object.fromEntries(\n Object.entries({\n correspondent: $json.correspondent_id,\n document_type: $json.document_type_id,\n storage_path: $json.storage_path_id,\n tags: $json.tag_ids && $json.tag_ids.length > 0 ? $json.tag_ids : undefined,\n created: $json.created_date || undefined\n }).filter(([_, v]) => v !== null && v !== undefined)\n )\n )\n}}", + "options": {} + }, + "credentials": { + "httpHeaderAuth": { + "id": "uvGjLbrN5yQTQIzv", + "name": "Paperless-NGX API" + } + } + } + ], + "connections": { + "📎 Webhook Bolletta": { + "main": [ + [ + { + "node": "Gmail - Leggi Messaggio", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gmail - Leggi Messaggio": { + "main": [ + [ + { + "node": "Trova Allegato PDF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Trova Allegato PDF": { + "main": [ + [ + { + "node": "Gmail - Download Allegato", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gmail - Download Allegato": { + "main": [ + [ + { + "node": "Decodifica Allegato", + "type": "main", + "index": 0 + } + ] + ] + }, + "Decodifica Allegato": { + "main": [ + [ + { + "node": "Paperless - Corrispondenti", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Corrispondenti": { + "main": [ + [ + { + "node": "Paperless - Tipi Doc", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Tipi Doc": { + "main": [ + [ + { + "node": "Paperless - Tag", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Tag": { + "main": [ + [ + { + "node": "Paperless - Percorsi", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Percorsi": { + "main": [ + [ + { + "node": "Paperless - Cerca Simili", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Cerca Simili": { + "main": [ + [ + { + "node": "Build Prompt Inferenza", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Prompt Inferenza": { + "main": [ + [ + { + "node": "Ottieni Token Copilot", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ottieni Token Copilot": { + "main": [ + [ + { + "node": "GPT-4.1 - Inferisci Metadati", + "type": "main", + "index": 0 + } + ] + ] + }, + "GPT-4.1 - Inferisci Metadati": { + "main": [ + [ + { + "node": "Parse Risposta GPT", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Risposta GPT": { + "main": [ + [ + { + "node": "Prepara Upload", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Upload Documento": { + "main": [ + [ + { + "node": "Estrai Task ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepara Upload": { + "main": [ + [ + { + "node": "Paperless - Upload Documento", + "type": "main", + "index": 0 + } + ] + ] + }, + "Estrai Task ID": { + "main": [ + [ + { + "node": "⏳ Attendi Paperless", + "type": "main", + "index": 0 + } + ] + ] + }, + "⏳ Attendi Paperless": { + "main": [ + [ + { + "node": "Paperless - Stato Task", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Stato Task": { + "main": [ + [ + { + "node": "Estrai Document ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Estrai Document ID": { + "main": [ + [ + { + "node": "Paperless - Patch Metadati", + "type": "main", + "index": 0 + } + ] + ] + }, + "Paperless - Patch Metadati": { + "main": [ + [ + { + "node": "Telegram - Conferma Upload", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "cd5d2a62-eea4-419d-b65f-1b4bc134fd33", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": true +} \ No newline at end of file diff --git a/workflows/w0oJ1i6sESvaB5W1.json b/workflows/w0oJ1i6sESvaB5W1.json new file mode 100644 index 0000000..7eb1e96 --- /dev/null +++ b/workflows/w0oJ1i6sESvaB5W1.json @@ -0,0 +1,146 @@ +{ + "id": "w0oJ1i6sESvaB5W1", + "name": "⏰ Actual — Reminder Estratto Conto [Schedule]", + "nodes": [ + { + "id": "n_cron", + "name": "⏰ Cron Giornaliero", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 0, + 300 + ], + "parameters": { + "rule": { + "interval": [ + { + "triggerAtHour": 9, + "field": "hours" + } + ] + } + } + }, + { + "id": "n_verifica", + "name": "📋 Verifica Task Scaduto", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 240, + 300 + ], + "parameters": { + "jsCode": "\nconst TASK_NAME = 'Actual - Estratto conto';\nconst TASKLIST_NAME = 'Finanze';\nconst today = new Date();\ntoday.setHours(0,0,0,0);\n\ntry {\n // Get task lists\n const listsRes = await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'GET',\n url: 'https://tasks.googleapis.com/tasks/v1/users/@me/lists',\n json: true\n }\n );\n \n const taskList = (listsRes.items || []).find(l => l.title === TASKLIST_NAME);\n if (!taskList) {\n return [{ json: { send_reminder: false, reason: `Lista \"${TASKLIST_NAME}\" non trovata` } }];\n }\n \n // Get pending tasks\n const tasksRes = await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'GET',\n url: `https://tasks.googleapis.com/tasks/v1/lists/${taskList.id}/tasks?showCompleted=false&showHidden=false`,\n json: true\n }\n );\n \n const task = (tasksRes.items || []).find(t => t.title === TASK_NAME && t.status !== 'completed');\n if (!task) {\n return [{ json: { send_reminder: false, reason: 'Task non trovato o già completato' } }];\n }\n \n // Check if due date is today or past\n let isDue = false;\n let dueInfo = 'nessuna scadenza';\n if (task.due) {\n const dueDate = new Date(task.due);\n dueDate.setHours(0,0,0,0);\n isDue = dueDate <= today;\n dueInfo = task.due.split('T')[0];\n } else {\n // No due date set — send reminder anyway (task exists and is not done)\n isDue = true;\n }\n \n return [{ json: {\n send_reminder: isDue,\n task_id: task.id,\n tasklist_id: taskList.id,\n due: dueInfo,\n reason: isDue ? `Task scaduto il ${dueInfo}` : `Task in scadenza il ${dueInfo}`\n } }];\n} catch(e) {\n return [{ json: { send_reminder: false, reason: `Errore Tasks API: ${e.message}` } }];\n}\n", + "mode": "runOnceForAllItems" + } + }, + { + "id": "n_if_due", + "name": "❓ Task Scaduto?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 480, + 300 + ], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "cond_due", + "leftValue": "={{ $json.send_reminder }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + } + } + }, + { + "id": "n_reminder", + "name": "📱 Reminder Telegram", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [ + 720, + 180 + ], + "parameters": { + "chatId": "-4814221197", + "text": "📊 *Reminder: Estratto Conto*\n\nRicordati di scaricare e caricare l'estratto conto bancario!\n\nMandami il file CSV su Telegram con caption:\n`Estratto conto`\n\n_Il task \"Actual - Estratto conto\" risulta in scadenza — sarà marcato completato automaticamente dopo il caricamento._", + "additionalFields": { + "parse_mode": "Markdown" + } + }, + "credentials": { + "telegramApi": { + "id": "uTXHLqcCJxbOvqN3", + "name": "Telegram account" + } + } + } + ], + "connections": { + "⏰ Cron Giornaliero": { + "main": [ + [ + { + "node": "📋 Verifica Task Scaduto", + "type": "main", + "index": 0 + } + ] + ] + }, + "📋 Verifica Task Scaduto": { + "main": [ + [ + { + "node": "❓ Task Scaduto?", + "type": "main", + "index": 0 + } + ] + ] + }, + "❓ Task Scaduto?": { + "main": [ + [ + { + "node": "📱 Reminder Telegram", + "type": "main", + "index": 0 + } + ], + [] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "ed54aeb1-f6ad-4605-a102-09e177e6eb9d", + "owner": { + "type": "personal", + "projectId": "Hdttz401OqqtObPo", + "projectName": "Martin Tahiraj ", + "personalEmail": "tahiraj.martin@gmail.com" + }, + "parentFolderId": null, + "isArchived": false +} \ No newline at end of file