{
  "name": "WF3 - Consulta RAG | JurisAI",
  "nodes": [
    {
      "id": "sticky_wf3",
      "name": "\ud83d\udccb WF3 - Consulta RAG Principal",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -220,
        -140
      ],
      "parameters": {
        "content": "## WF3 \u2014 Consulta RAG Principal\n\n**Endpoint:** `POST /webhook/jurisai/query`  \n**Rate Limits:** estagi\u00e1rio=10rpm \u00b7 advogado=25rpm \u00b7 curador=50rpm \u00b7 admin=100rpm\n\n**Body esperado:**\n```json\n{\n  \"query\": \"texto da pergunta\",\n  \"session_id\": \"uuid-da-sessao\",\n  \"legal_area\": [\"urbanistico\"],\n  \"territory\": \"federal\",\n  \"uf\": \"SP\" \n}\n```\n\n**Fluxo:**\n1. JWT validate \u2192 rate limit\n2. Roteamento de inten\u00e7\u00e3o (Haiku)\n3. Busca hist\u00f3rico da sess\u00e3o\n4. Embed query \u2192 Qdrant search (status=em_vigor)\n5. Rerank top 8 \u2192 preparar contexto\n6. AI Agent (Claude Sonnet) via LangChain\n7. SHA-256 query, log LGPD-compliant\n8. Salvar sess\u00e3o + responder\n\n\u2699\ufe0f Configure credencial Anthropic no n\u00f3 'Claude Sonnet (LLM)'",
        "height": 370,
        "width": 470,
        "color": 6
      }
    },
    {
      "id": "webhook_wf3",
      "name": "Webhook Consulta",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        250,
        400
      ],
      "parameters": {
        "path": "jurisai/query",
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      }
    },
    {
      "id": "validate_jwt_wf3",
      "name": "Validar JWT",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        400
      ],
      "parameters": {
        "jsCode": "const authHeader = $('Webhook Consulta').first().json.headers?.authorization || '';\nconst token = authHeader.replace(/^Bearer\\s+/i, '').trim();\nif (!token) throw new Error('401:Token de autentica\u00e7\u00e3o n\u00e3o fornecido');\n\ntry {\n  const parts = token.split('.');\n  if (parts.length !== 3) throw new Error('401:Token JWT inv\u00e1lido');\n  const payloadB64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');\n  const padded = payloadB64 + '='.repeat((4 - (payloadB64.length % 4)) % 4);\n  const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));\n  if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) throw new Error('401:Token expirado');\n\n  const userId = payload.sub;\n  const role = payload.user_metadata?.role || payload.role || 'estagiario';\n  const body = $('Webhook Consulta').first().json.body || {};\n\n  if (!body.query || body.query.trim().length < 10) throw new Error('400:Query deve ter pelo menos 10 caracteres');\n  if (body.query.length > 2000) throw new Error('400:Query n\u00e3o pode exceder 2000 caracteres');\n\n  const rateLimits = { estagiario: 10, advogado: 25, curador: 50, admin: 100 };\n  const rpmLimit = rateLimits[role] || 10;\n\n  return [{ json: {\n    user_id: userId,\n    role,\n    rpm_limit: rpmLimit,\n    query: body.query.trim(),\n    session_id: body.session_id || `sess_${userId}_${Date.now()}`,\n    legal_area: Array.isArray(body.legal_area) ? body.legal_area : (body.legal_area ? [body.legal_area] : []),\n    territory: body.territory || null,\n    uf: body.uf || null\n  } }];\n} catch(e) {\n  const msg = e.message || 'Erro';\n  if (/^(400|401|403):/.test(msg)) throw new Error(msg);\n  throw new Error('401:Falha na valida\u00e7\u00e3o do token');\n}"
      }
    },
    {
      "id": "check_rate_limit",
      "name": "Verificar Rate Limit (Supabase)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        750,
        400
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/query_logs?user_id=eq.{{ $json.user_id }}&created_at=gte.{{ new Date(Date.now() - 60000).toISOString() }}&select=id",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Prefer",
              "value": "count=exact"
            }
          ]
        }
      },
      "continueOnFail": true
    },
    {
      "id": "eval_rate_limit",
      "name": "Rate Limit Excedido?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1000,
        400
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_rate",
              "leftValue": "={{ ($('Verificar Rate Limit (Supabase)').first().json?.length || 0) >= $('Validar JWT').first().json.rpm_limit }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "respond_rate_limit",
      "name": "Responder 429 Rate Limit",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        1250,
        520
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: 'Rate limit excedido. Tente novamente em alguns instantes.', rpm_limit: $('Validar JWT').first().json.rpm_limit }) }}",
        "options": {
          "responseCode": 429
        }
      }
    },
    {
      "id": "route_intent",
      "name": "Rotear Inten\u00e7\u00e3o (Claude Haiku)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1250,
        300
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ JSON.stringify({ model: $env.LLM_TRIAGE, max_tokens: 64, temperature: 0, system: 'Classifique a consulta jur\u00eddica. Responda APENAS com uma destas palavras: consulta, analise, producao_doc, multitematica, fora_do_escopo. Escopo: direito urban\u00edstico, imobili\u00e1rio, tribut\u00e1rio, trabalhista, contratual, civil, penal e administrativo brasileiro.', messages: [{ role: 'user', content: $('Validar JWT').first().json.query }] }) }}"
      }
    },
    {
      "id": "parse_intent",
      "name": "Parse Inten\u00e7\u00e3o",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1500,
        300
      ],
      "parameters": {
        "jsCode": "const text = ($('Rotear Inten\u00e7\u00e3o (Claude Haiku)').first().json?.content?.[0]?.text || 'consulta').trim().toLowerCase();\nconst validTypes = ['consulta', 'analise', 'producao_doc', 'multitematica', 'fora_do_escopo'];\nconst intent = validTypes.includes(text) ? text : 'consulta';\nconst jwtData = $('Validar JWT').first().json;\nreturn [{ json: { ...jwtData, intent, is_out_of_scope: intent === 'fora_do_escopo' } }];"
      }
    },
    {
      "id": "if_out_of_scope",
      "name": "Fora do Escopo?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1750,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_scope",
              "leftValue": "={{ $json.is_out_of_scope }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "respond_out_of_scope",
      "name": "Responder Fora do Escopo",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        2000,
        450
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, intent: 'fora_do_escopo', error: 'Esta consulta est\u00e1 fora do escopo jur\u00eddico do JurisAI. Por favor, fa\u00e7a perguntas sobre direito urban\u00edstico, imobili\u00e1rio, tribut\u00e1rio, trabalhista, contratual, civil, penal ou administrativo brasileiro.' }) }}",
        "options": {
          "responseCode": 422
        }
      }
    },
    {
      "id": "get_history",
      "name": "Buscar Hist\u00f3rico da Sess\u00e3o",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2000,
        250
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/chat_sessions?session_id=eq.{{ $json.session_id }}&user_id=eq.{{ $json.user_id }}&select=role,content&order=created_at.desc&limit=10",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "={{ $env.SUPABASE_SERVICE_KEY }}"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.SUPABASE_SERVICE_KEY }}"
            }
          ]
        }
      },
      "continueOnFail": true
    },
    {
      "id": "embed_query",
      "name": "Gerar Embedding da Query",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2250,
        250
      ],
      "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: $('Parse Inten\u00e7\u00e3o').first().json.query }) }}"
      }
    },
    {
      "id": "search_qdrant",
      "name": "Buscar no Qdrant",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2500,
        250
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.QDRANT_URL }}/collections/{{ $env.QDRANT_COLLECTION }}/points/search",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "api-key",
              "value": "={{ $env.QDRANT_API_KEY }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "body": "={{ (() => { const intentData = $('Parse Inten\u00e7\u00e3o').first().json; const embedding = $json.data?.[0]?.embedding || []; const mustFilters = [{ key: 'status', match: { value: 'em_vigor' } }]; if (intentData.legal_area && intentData.legal_area.length > 0) { mustFilters.push({ key: 'legal_area', match: { any: intentData.legal_area } }); } if (intentData.uf) { mustFilters.push({ key: 'uf', match: { value: intentData.uf } }); } return JSON.stringify({ vector: embedding, limit: 10, with_payload: true, with_vector: false, filter: { must: mustFilters } }); })() }}"
      }
    },
    {
      "id": "prepare_context",
      "name": "Reranking e Prepara\u00e7\u00e3o do Contexto",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2750,
        250
      ],
      "parameters": {
        "jsCode": "const intentData = $('Parse Inten\u00e7\u00e3o').first().json;\nconst rawQuery = intentData.query;\nconst qdrantResults = $('Buscar no Qdrant').first().json?.result || [];\nconst historyRaw = $('Buscar Hist\u00f3rico da Sess\u00e3o').first().json || [];\nconst history = Array.isArray(historyRaw) ? historyRaw.reverse() : [];\n\n// Rerank por score e relev\u00e2ncia (top 8)\nconst ranked = qdrantResults\n  .sort((a, b) => (b.score || 0) - (a.score || 0))\n  .slice(0, 8);\n\n// Montar contexto\nconst contextChunks = ranked.map((r, i) => {\n  const p = r.payload || {};\n  return `[${i+1}] ${p.doc_type?.toUpperCase() || 'DOC'} | ${p.issuer || ''} | ${p.published_at || ''} | ${p.territory || ''}\\n${p.text || ''}`;\n}).join('\\n\\n---\\n\\n');\n\n// Fontes para cita\u00e7\u00e3o\nconst sources = ranked.map(r => ({\n  document_id: r.payload?.document_id,\n  doc_type: r.payload?.doc_type,\n  issuer: r.payload?.issuer,\n  published_at: r.payload?.published_at,\n  score: r.score\n}));\n\n// Hist\u00f3rico de conversa formatado\nconst chatHistory = history.slice(-8).map(m => `${m.role === 'user' ? 'Usu\u00e1rio' : 'JurisAI'}: ${m.content}`).join('\\n');\n\n// System prompt\nconst systemPrompt = `Voc\u00ea \u00e9 o JurisAI, assistente jur\u00eddico especializado em direito brasileiro. Responda com precis\u00e3o e cite os documentos fornecidos usando [N] onde N \u00e9 o n\u00famero do documento.\n\nDOCUMENTOS DO CORPUS:\n${contextChunks || 'Nenhum documento relevante encontrado no corpus.'}\n\nINSTRU\u00c7\u00d5ES:\n- Seja preciso e cite sempre as fontes com [N]\n- Se n\u00e3o encontrar informa\u00e7\u00e3o no corpus, declare explicitamente\n- Use linguagem jur\u00eddica apropriada mas acess\u00edvel\n- Limite a resposta a 800 palavras`;\n\n// Human prompt com hist\u00f3rico\nconst humanPrompt = chatHistory \n  ? `HIST\u00d3RICO DESTA SESS\u00c3O:\\n${chatHistory}\\n\\nNOVA PERGUNTA: ${rawQuery}`\n  : rawQuery;\n\nreturn [{ json: {\n  ...intentData,\n  systemPrompt,\n  humanPrompt,\n  sources,\n  context_count: ranked.length\n} }];"
      }
    },
    {
      "id": "lm_claude_sonnet",
      "name": "Claude Sonnet (LLM)",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        3000,
        500
      ],
      "parameters": {
        "model": "={{ $env.LLM_MAIN || 'claude-sonnet-4-5' }}",
        "options": {
          "maxTokens": 2048,
          "temperature": 0.3
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "ai_agent_rag",
      "name": "AI Agent RAG",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.7,
      "position": [
        3000,
        250
      ],
      "parameters": {
        "agent": "conversationalAgent",
        "promptType": "define",
        "text": "={{ $json.humanPrompt }}",
        "hasOutputParser": false,
        "options": {
          "systemMessage": "={{ $json.systemPrompt }}",
          "maxIterations": 3,
          "returnIntermediateSteps": false
        }
      }
    },
    {
      "id": "parse_agent_response",
      "name": "Parsear Resposta e Cita\u00e7\u00f5es",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3250,
        250
      ],
      "parameters": {
        "jsCode": "const agentOutput = $json.output || $json.text || '';\nconst intentData = $('Reranking e Prepara\u00e7\u00e3o do Contexto').first().json;\n\n// Extrair cita\u00e7\u00f5es [N] usadas na resposta\nconst citationMatches = [...agentOutput.matchAll(/\\[(\\d+)\\]/g)];\nconst citedIndices = [...new Set(citationMatches.map(m => parseInt(m[1]) - 1))];\nconst usedSources = citedIndices\n  .map(i => intentData.sources[i])\n  .filter(Boolean);\n\n// SHA-256 da query para LGPD\nconst crypto = require('crypto');\nconst queryHash = crypto.createHash('sha256').update(intentData.query).digest('hex');\n\nreturn [{ json: {\n  user_id: intentData.user_id,\n  role: intentData.role,\n  session_id: intentData.session_id,\n  intent: intentData.intent,\n  query_hash: queryHash,\n  response: agentOutput,\n  sources: usedSources,\n  context_count: intentData.context_count\n} }];"
      }
    },
    {
      "id": "save_session_user",
      "name": "Salvar Mensagem Usu\u00e1rio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3500,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/chat_sessions",
        "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({ session_id: $json.session_id, user_id: $json.user_id, role: 'user', content: $('Reranking e Prepara\u00e7\u00e3o do Contexto').first().json.query }) }}"
      },
      "continueOnFail": true
    },
    {
      "id": "save_session_assistant",
      "name": "Salvar Resposta JurisAI",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3500,
        320
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/chat_sessions",
        "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({ session_id: $json.session_id, user_id: $json.user_id, role: 'assistant', content: $json.response }) }}"
      },
      "continueOnFail": true
    },
    {
      "id": "log_query",
      "name": "Log LGPD (query_logs)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3750,
        260
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SUPABASE_URL }}/rest/v1/query_logs",
        "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 d = $('Parsear Resposta e Cita\u00e7\u00f5es').first().json; return JSON.stringify({ user_id: d.user_id, session_id: d.session_id, query_hash: d.query_hash, query_type: d.intent, docs_retrieved: d.context_count, created_at: new Date().toISOString() }); })() }}"
      },
      "continueOnFail": true
    },
    {
      "id": "respond_success_wf3",
      "name": "Responder Consulta",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        4000,
        260
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ (() => { const d = $('Parsear Resposta e Cita\u00e7\u00f5es').first().json; const logId = $('Log LGPD (query_logs)').first().json?.[0]?.id || null; return JSON.stringify({ success: true, response: d.response, sources: d.sources, intent: d.intent, session_id: d.session_id, log_id: logId }); })() }}",
        "options": {
          "responseCode": 200
        }
      }
    }
  ],
  "connections": {
    "Webhook Consulta": {
      "main": [
        [
          {
            "node": "Validar JWT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar JWT": {
      "main": [
        [
          {
            "node": "Verificar Rate Limit (Supabase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verificar Rate Limit (Supabase)": {
      "main": [
        [
          {
            "node": "Rate Limit Excedido?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Excedido?": {
      "main": [
        [
          {
            "node": "Rotear Inten\u00e7\u00e3o (Claude Haiku)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder 429 Rate Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rotear Inten\u00e7\u00e3o (Claude Haiku)": {
      "main": [
        [
          {
            "node": "Parse Inten\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Inten\u00e7\u00e3o": {
      "main": [
        [
          {
            "node": "Fora do Escopo?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fora do Escopo?": {
      "main": [
        [
          {
            "node": "Buscar Hist\u00f3rico da Sess\u00e3o",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder Fora do Escopo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar Hist\u00f3rico da Sess\u00e3o": {
      "main": [
        [
          {
            "node": "Gerar Embedding da Query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gerar Embedding da Query": {
      "main": [
        [
          {
            "node": "Buscar no Qdrant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar no Qdrant": {
      "main": [
        [
          {
            "node": "Reranking e Prepara\u00e7\u00e3o do Contexto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reranking e Prepara\u00e7\u00e3o do Contexto": {
      "main": [
        [
          {
            "node": "AI Agent RAG",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Sonnet (LLM)": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent RAG",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent RAG": {
      "main": [
        [
          {
            "node": "Parsear Resposta e Cita\u00e7\u00f5es",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parsear Resposta e Cita\u00e7\u00f5es": {
      "main": [
        [
          {
            "node": "Salvar Mensagem Usu\u00e1rio",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Salvar Resposta JurisAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salvar Mensagem Usu\u00e1rio": {
      "main": [
        [
          {
            "node": "Log LGPD (query_logs)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salvar Resposta JurisAI": {
      "main": [
        [
          {
            "node": "Log LGPD (query_logs)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log LGPD (query_logs)": {
      "main": [
        [
          {
            "node": "Responder Consulta",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": false,
    "errorWorkflow": ""
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": [
    {
      "id": "jurisai",
      "name": "JurisAI"
    },
    {
      "id": "rag",
      "name": "rag-query"
    }
  ]
}