{
  "name": "WF1 - Ingest\u00e3o Autom\u00e1tica | JurisAI",
  "nodes": [
    {
      "id": "sticky_wf1",
      "name": "\ud83d\udccb WF1 - Ingest\u00e3o Autom\u00e1tica",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -220,
        -100
      ],
      "parameters": {
        "content": "## WF1 \u2014 Ingest\u00e3o Autom\u00e1tica de Corpus\n\n**Trigger:** Schedule di\u00e1rio \u00e0s 06:00 UTC  \n**Fonte:** Tabela `ingestion_sources` (where active = true)  \n\n**Fluxo por fonte:**\n1. Download do documento (PDF ou HTML)\n2. Extra\u00e7\u00e3o de texto\n3. Chunking (1500 chars, overlap 200)\n4. Embeddings via OpenAI\n5. Upsert no Qdrant com payload completo\n6. INSERT no Supabase documents\n7. Chama WF6 para verificar vig\u00eancia\n8. Atualiza `last_run_at` da fonte\n\n\u2699\ufe0f **Ap\u00f3s importar:**\n- Ajuste o SMTP para notifica\u00e7\u00f5es\n- Preencha a tabela `ingestion_sources` no Supabase\n- Configure o ID do WF6 em 'Verificar Vig\u00eancia'",
        "height": 320,
        "width": 430,
        "color": 4
      }
    },
    {
      "id": "schedule_wf1",
      "name": "Trigger Di\u00e1rio",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        250,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 6,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "id": "get_sources",
      "name": "Buscar Fontes Ativas",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        500,
        300
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/ingestion_sources",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
            }
          ]
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "active",
              "value": "eq.true"
            },
            {
              "name": "select",
              "value": "id,name,source_url,doc_type,legal_area,territory,uf,issuer,schedule_cron"
            },
            {
              "name": "order",
              "value": "last_run_at.asc.nullsfirst"
            },
            {
              "name": "limit",
              "value": "20"
            }
          ]
        }
      }
    },
    {
      "id": "check_sources",
      "name": "H\u00e1 Fontes?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        750,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_sources",
              "leftValue": "={{ $input.all().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "split_sources",
      "name": "Processar Fonte por Fonte",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1000,
        200
      ],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "download_doc",
      "name": "Download do Documento",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1250,
        200
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.source_url }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": false,
              "responseFormat": "file"
            }
          }
        }
      },
      "continueOnFail": true
    },
    {
      "id": "check_download",
      "name": "Download OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1500,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_download",
              "leftValue": "={{ $json.error }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "isEmpty"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "extract_pdf",
      "name": "Extrair Texto (PDF)",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        1750,
        100
      ],
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "data",
        "options": {}
      },
      "continueOnFail": true
    },
    {
      "id": "chunk_and_prepare",
      "name": "Chunking e Prepara\u00e7\u00e3o",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        100
      ],
      "parameters": {
        "jsCode": "const source = $('Processar Fonte por Fonte').first().json;\nconst rawText = $json.text || '';\n\nif (!rawText || rawText.length < 100) {\n  throw new Error('Texto insuficiente para ingest\u00e3o: ' + source.source_url);\n}\n\nconst CHUNK_SIZE = 1500;\nconst OVERLAP = 200;\nconst chunks = [];\nlet start = 0;\n\nwhile (start < rawText.length) {\n  const end = Math.min(start + CHUNK_SIZE, rawText.length);\n  const chunk = rawText.slice(start, end).trim();\n  if (chunk.length > 100) {\n    chunks.push({ text: chunk, chunk_index: chunks.length });\n  }\n  start += CHUNK_SIZE - OVERLAP;\n}\n\nconst docId = `auto_${source.id}_${Date.now()}`;\nconst title = source.name || `${source.doc_type} - ${source.issuer}`;\nconst today = new Date().toISOString().split('T')[0];\n\nreturn chunks.map(chunk => ({\n  json: {\n    doc_id: docId,\n    source_id: source.id,\n    source_url: source.source_url,\n    title,\n    doc_type: source.doc_type,\n    legal_area: Array.isArray(source.legal_area) ? source.legal_area : [source.legal_area || 'administrativo'],\n    territory: source.territory || 'federal',\n    uf: source.uf || null,\n    issuer: source.issuer,\n    published_at: source.published_at || today,\n    total_chunks: chunks.length,\n    ...chunk\n  }\n}));"
      }
    },
    {
      "id": "embed_wf1",
      "name": "Gerar Embeddings",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2250,
        100
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/embeddings",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.OPENAI_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ JSON.stringify({ model: $env.EMBEDDING_MODEL, input: $json.text }) }}"
      }
    },
    {
      "id": "upsert_qdrant_wf1",
      "name": "Upsert no Qdrant",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2500,
        100
      ],
      "parameters": {
        "method": "PUT",
        "url": "={{ $env.QDRANT_URL }}/collections/{{ $env.QDRANT_COLLECTION }}/points",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "api-key",
              "value": "={{ $env.QDRANT_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ (() => { const c = $('Chunking e Prepara\u00e7\u00e3o').first().json; const emb = $json.data?.[0]?.embedding || []; return JSON.stringify({ points: [{ id: c.doc_id + '_chunk_' + c.chunk_index, vector: emb, payload: { document_id: c.doc_id, source_id: c.source_id, chunk_index: c.chunk_index, text: c.text, doc_type: c.doc_type, legal_area: c.legal_area, territory: c.territory, uf: c.uf, issuer: c.issuer, published_at: c.published_at, status: 'em_vigor' } }] }); })() }}"
      },
      "continueOnFail": true
    },
    {
      "id": "aggregate_wf1",
      "name": "Agregar Chunks",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        2750,
        100
      ],
      "parameters": {
        "aggregate": "aggregateIndividualFields",
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "doc_id",
              "renameField": false
            }
          ]
        },
        "options": {
          "mergeLists": false
        }
      }
    },
    {
      "id": "insert_doc_wf1",
      "name": "Registrar Documento (Supabase)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3000,
        100
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/documents",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=representation"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ (() => { const c = $('Chunking e Prepara\u00e7\u00e3o').first().json; return JSON.stringify({ id: c.doc_id, title: c.title, doc_type: c.doc_type, legal_area: c.legal_area, territory: c.territory, uf: c.uf, issuer: c.issuer, published_at: c.published_at, source_url: c.source_url, status: 'em_vigor', ingested_at: new Date().toISOString() }); })() }}"
      },
      "continueOnFail": true
    },
    {
      "id": "call_wf6",
      "name": "Verificar Vig\u00eancia (WF6)",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.1,
      "position": [
        3250,
        100
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "value": "CONFIGURE_WF6_ID",
          "mode": "id"
        },
        "waitForSubWorkflow": false,
        "options": {
          "loadedWorkflowData": {
            "source_url": "={{ $('Chunking e Prepara\u00e7\u00e3o').first().json.source_url }}",
            "document_id": "={{ $('Chunking e Prepara\u00e7\u00e3o').first().json.doc_id }}"
          }
        }
      },
      "continueOnFail": true
    },
    {
      "id": "update_source",
      "name": "Atualizar last_run_at",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3500,
        100
      ],
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/ingestion_sources?id=eq.{{ $('Processar Fonte por Fonte').first().json.id }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ JSON.stringify({ last_run_at: new Date().toISOString(), last_doc_count: $('Chunking e Prepara\u00e7\u00e3o').all().length }) }}"
      },
      "continueOnFail": true
    },
    {
      "id": "log_download_error",
      "name": "Log Erro de Download",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1750,
        300
      ],
      "parameters": {
        "method": "PATCH",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/ingestion_sources?id=eq.{{ $('Processar Fonte por Fonte').first().json.id }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ JSON.stringify({ last_run_at: new Date().toISOString(), last_error: 'Falha no download: ' + ($('Download do Documento').first().json.error?.message || 'erro desconhecido') }) }}"
      },
      "continueOnFail": true
    },
    {
      "id": "no_sources",
      "name": "Sem Fontes Ativas",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1000,
        400
      ],
      "parameters": {}
    },
    {
      "id": "notify_ingestion",
      "name": "Notificar Ingest\u00e3o Conclu\u00edda",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        3750,
        100
      ],
      "parameters": {
        "fromEmail": "jurisai@{{ $env.ORG_NAME || 'jurisai.app' }}",
        "toEmail": "={{ $env.CURATOR_EMAIL || 'curador@suaorganizacao.com' }}",
        "subject": "[JurisAI] Ingest\u00e3o di\u00e1ria conclu\u00edda",
        "emailType": "text",
        "message": "=Ingest\u00e3o Autom\u00e1tica \u2014 Relat\u00f3rio Di\u00e1rio\n\nData: {{ new Date().toLocaleDateString('pt-BR') }}\n\nDocumentos processados com sucesso na \u00faltima execu\u00e7\u00e3o.\n\nAcesse o painel para mais detalhes.\n\n---\nJurisAI - {{ $env.ORG_NAME }}"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    }
  ],
  "connections": {
    "Trigger Di\u00e1rio": {
      "main": [
        [
          {
            "node": "Buscar Fontes Ativas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar Fontes Ativas": {
      "main": [
        [
          {
            "node": "H\u00e1 Fontes?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "H\u00e1 Fontes?": {
      "main": [
        [
          {
            "node": "Processar Fonte por Fonte",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Sem Fontes Ativas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Processar Fonte por Fonte": {
      "main": [
        [
          {
            "node": "Download do Documento",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download do Documento": {
      "main": [
        [
          {
            "node": "Download OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download OK?": {
      "main": [
        [
          {
            "node": "Extrair Texto (PDF)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Erro de Download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extrair Texto (PDF)": {
      "main": [
        [
          {
            "node": "Chunking e Prepara\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chunking e Prepara\u00e7\u00e3o": {
      "main": [
        [
          {
            "node": "Gerar Embeddings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gerar Embeddings": {
      "main": [
        [
          {
            "node": "Upsert no Qdrant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert no Qdrant": {
      "main": [
        [
          {
            "node": "Agregar Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agregar Chunks": {
      "main": [
        [
          {
            "node": "Registrar Documento (Supabase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar Documento (Supabase)": {
      "main": [
        [
          {
            "node": "Verificar Vig\u00eancia (WF6)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verificar Vig\u00eancia (WF6)": {
      "main": [
        [
          {
            "node": "Atualizar last_run_at",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Atualizar last_run_at": {
      "main": [
        [
          {
            "node": "Notificar Ingest\u00e3o Conclu\u00edda",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": false,
    "errorWorkflow": ""
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": [
    {
      "id": "jurisai",
      "name": "JurisAI"
    },
    {
      "id": "ingestion",
      "name": "ingestion-auto"
    }
  ]
}