feat(flows):allineamento filtri

This commit is contained in:
2026-03-21 20:30:54 +00:00
parent 8394861bb3
commit 734a1a9440
14 changed files with 4186 additions and 144 deletions

View File

@@ -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(/<thinking>[\\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",

View File

@@ -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\": \"<uid originale>\",\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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": false
}

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": false
}

View File

@@ -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",

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": false
}

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": false
}

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": true
}

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": false
}

File diff suppressed because one or more lines are too long

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": true
}

View File

@@ -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 <tahiraj.martin@gmail.com>",
"personalEmail": "tahiraj.martin@gmail.com"
},
"parentFolderId": null,
"isArchived": false
}