AutomationFlowsSlack & Telegram › Run Multi-model Research Analysis and Email Reports with Gpt-4, Claude and…

Run Multi-model Research Analysis and Email Reports with Gpt-4, Claude and…

Original n8n title: Run Multi-model Research Analysis and Email Reports with Gpt-4, Claude and Nvidia Nim

ByCheng Siong Chin @cschin on n8n.io

This workflow automates end-to-end research analysis by coordinating multiple AI models—including NVIDIA NIM (Llama), OpenAI GPT-4, and Claude to analyze uploaded documents, extract insights, and generate polished reports delivered via email. Built for researchers, academics,…

Webhook trigger★★★★★ complexity46 nodesHTTP RequestPostgresSlackEmail Send
Slack & Telegram Trigger: Webhook Nodes: 46 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #12537 — we link there as the canonical source.

This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "aqlIB54QNx9xtMjo",
  "name": "AI-Powered Multi-Model Research Analysis & Report Generation",
  "tags": [],
  "nodes": [
    {
      "id": "7f8374e8-47be-476b-af21-84099ffe0241",
      "name": "LLM Request Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        928,
        1960
      ],
      "parameters": {
        "path": "llm-orchestration",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "baa4054a-1a64-466d-b099-3f5e3a91241b",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        1152,
        1960
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "azureHealthEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Azure OpenAI health check endpoint URL__>"
            },
            {
              "id": "id-2",
              "name": "awsHealthEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__AWS Bedrock health check endpoint URL__>"
            },
            {
              "id": "id-3",
              "name": "googleHealthEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Google Vertex AI health check endpoint URL__>"
            },
            {
              "id": "id-4",
              "name": "localHealthEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Local model health check endpoint URL__>"
            },
            {
              "id": "id-5",
              "name": "azureApiEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Azure OpenAI API endpoint URL__>"
            },
            {
              "id": "id-6",
              "name": "awsApiEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__AWS Bedrock API endpoint URL__>"
            },
            {
              "id": "id-7",
              "name": "googleApiEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Google Vertex AI API endpoint URL__>"
            },
            {
              "id": "id-8",
              "name": "localApiEndpoint",
              "type": "string",
              "value": "<__PLACEHOLDER_VALUE__Local model API endpoint URL__>"
            },
            {
              "id": "id-9",
              "name": "maxRetries",
              "type": "number",
              "value": 3
            },
            {
              "id": "id-10",
              "name": "retryBaseDelay",
              "type": "number",
              "value": 1000
            },
            {
              "id": "id-11",
              "name": "circuitBreakerThreshold",
              "type": "number",
              "value": 5
            },
            {
              "id": "id-12",
              "name": "anomalyThreshold",
              "type": "number",
              "value": 0.8
            },
            {
              "id": "id-13",
              "name": "costCeilingDefault",
              "type": "number",
              "value": 1
            },
            {
              "id": "id-14",
              "name": "latencySLODefault",
              "type": "number",
              "value": 5000
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "5616905c-5cbe-449d-a14c-41bb52a96ef3",
      "name": "Parse Request & Validate",
      "type": "n8n-nodes-base.code",
      "position": [
        1376,
        1960
      ],
      "parameters": {
        "jsCode": "// Parse and validate incoming webhook request\n// Extract task metadata, regulatory constraints, cost ceilings, latency SLOs, quality expectations, and tenant ID\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const body = item.json.body || item.json;\n  \n  // Initialize validation errors array\n  const validationErrors = [];\n  \n  // Parse and validate required fields\n  const prompt = body.prompt || body.query || body.text;\n  if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {\n    validationErrors.push('Missing or invalid prompt/query/text');\n  }\n  \n  const tenantId = body.tenantId || body.tenant_id || body.tenant;\n  if (!tenantId) {\n    validationErrors.push('Missing tenant ID');\n  }\n  \n  // Parse task metadata\n  const taskMetadata = {\n    taskId: body.taskId || body.task_id || `task_${Date.now()}`,\n    taskType: body.taskType || body.task_type || 'general',\n    priority: body.priority || 'medium',\n    timestamp: new Date().toISOString()\n  };\n  \n  // Parse regulatory constraints\n  const regulatoryConstraints = {\n    dataResidency: body.dataResidency || body.data_residency || 'any',\n    complianceRequirements: body.complianceRequirements || body.compliance_requirements || [],\n    piiHandling: body.piiHandling || body.pii_handling || 'standard',\n    dataClassification: body.dataClassification || body.data_classification || 'internal'\n  };\n  \n  // Parse cost ceilings\n  const costCeilings = {\n    maxCostPerRequest: parseFloat(body.maxCostPerRequest || body.max_cost_per_request || 0.10),\n    budgetLimit: parseFloat(body.budgetLimit || body.budget_limit || 100.0),\n    costOptimizationEnabled: body.costOptimizationEnabled !== false\n  };\n  \n  // Parse latency SLOs\n  const latencySLOs = {\n    maxLatencyMs: parseInt(body.maxLatencyMs || body.max_latency_ms || 5000),\n    p95LatencyMs: parseInt(body.p95LatencyMs || body.p95_latency_ms || 3000),\n    p99LatencyMs: parseInt(body.p99LatencyMs || body.p99_latency_ms || 4000),\n    timeoutMs: parseInt(body.timeoutMs || body.timeout_ms || 30000)\n  };\n  \n  // Parse quality expectations\n  const qualityExpectations = {\n    minConfidenceScore: parseFloat(body.minConfidenceScore || body.min_confidence_score || 0.7),\n    requireFactChecking: body.requireFactChecking || body.require_fact_checking || false,\n    hallucinationTolerance: body.hallucinationTolerance || body.hallucination_tolerance || 'low',\n    outputFormat: body.outputFormat || body.output_format || 'json',\n    maxTokens: parseInt(body.maxTokens || body.max_tokens || 2000),\n    temperature: parseFloat(body.temperature || 0.7)\n  };\n  \n  // Validate numeric ranges\n  if (costCeilings.maxCostPerRequest < 0) {\n    validationErrors.push('maxCostPerRequest must be non-negative');\n  }\n  \n  if (latencySLOs.maxLatencyMs < 100 || latencySLOs.maxLatencyMs > 60000) {\n    validationErrors.push('maxLatencyMs must be between 100 and 60000');\n  }\n  \n  if (qualityExpectations.minConfidenceScore < 0 || qualityExpectations.minConfidenceScore > 1) {\n    validationErrors.push('minConfidenceScore must be between 0 and 1');\n  }\n  \n  if (qualityExpectations.temperature < 0 || qualityExpectations.temperature > 2) {\n    validationErrors.push('temperature must be between 0 and 2');\n  }\n  \n  // Build validated request object\n  const validatedRequest = {\n    prompt,\n    tenantId,\n    taskMetadata,\n    regulatoryConstraints,\n    costCeilings,\n    latencySLOs,\n    qualityExpectations,\n    validationErrors,\n    isValid: validationErrors.length === 0,\n    rawRequest: body\n  };\n  \n  results.push({\n    json: validatedRequest\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "3f668bc0-5e9a-4e27-9d62-b9ce59691728",
      "name": "Analyze Prompt Complexity",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        1960
      ],
      "parameters": {
        "jsCode": "// Analyze Prompt Complexity\n// This code analyzes incoming prompts for token count, semantic complexity, domain classification, and processing time estimation\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const prompt = item.json.prompt || item.json.text || item.json.message || '';\n  \n  // Token count estimation (rough approximation: ~4 chars per token)\n  const tokenCount = Math.ceil(prompt.length / 4);\n  \n  // Semantic complexity score (0-100)\n  // Based on: sentence count, avg word length, unique words, punctuation density\n  const sentences = prompt.split(/[.!?]+/).filter(s => s.trim().length > 0);\n  const words = prompt.split(/\\s+/).filter(w => w.length > 0);\n  const uniqueWords = new Set(words.map(w => w.toLowerCase()));\n  const avgWordLength = words.reduce((sum, w) => sum + w.length, 0) / (words.length || 1);\n  const punctuationCount = (prompt.match(/[,;:()\\[\\]{}\"']/g) || []).length;\n  const punctuationDensity = punctuationCount / (words.length || 1);\n  \n  const complexityScore = Math.min(100, Math.round(\n    (sentences.length * 5) +\n    (avgWordLength * 8) +\n    ((uniqueWords.size / (words.length || 1)) * 30) +\n    (punctuationDensity * 20)\n  ));\n  \n  // Domain classification\n  const domainKeywords = {\n    technical: ['code', 'function', 'algorithm', 'database', 'api', 'programming', 'software', 'debug', 'error', 'system'],\n    creative: ['write', 'story', 'poem', 'creative', 'imagine', 'describe', 'narrative', 'character', 'plot'],\n    analytical: ['analyze', 'compare', 'evaluate', 'assess', 'calculate', 'determine', 'measure', 'statistics', 'data'],\n    conversational: ['hello', 'hi', 'thanks', 'please', 'help', 'question', 'what', 'how', 'why', 'explain'],\n    business: ['strategy', 'market', 'revenue', 'customer', 'sales', 'business', 'profit', 'growth', 'roi']\n  };\n  \n  const lowerPrompt = prompt.toLowerCase();\n  const domainScores = {};\n  \n  for (const [domain, keywords] of Object.entries(domainKeywords)) {\n    const matchCount = keywords.filter(kw => lowerPrompt.includes(kw)).length;\n    domainScores[domain] = matchCount;\n  }\n  \n  const primaryDomain = Object.entries(domainScores)\n    .sort((a, b) => b[1] - a[1])[0]?.[0] || 'general';\n  \n  // Estimated processing time (in seconds)\n  // Base time + token factor + complexity factor\n  const baseTime = 0.5;\n  const tokenFactor = tokenCount * 0.01;\n  const complexityFactor = complexityScore * 0.02;\n  const estimatedProcessingTime = Math.round((baseTime + tokenFactor + complexityFactor) * 100) / 100;\n  \n  results.push({\n    json: {\n      ...item.json,\n      promptAnalysis: {\n        tokenCount,\n        complexityScore,\n        primaryDomain,\n        domainScores,\n        estimatedProcessingTime,\n        promptLength: prompt.length,\n        sentenceCount: sentences.length,\n        wordCount: words.length,\n        uniqueWordRatio: Math.round((uniqueWords.size / (words.length || 1)) * 100) / 100\n      }\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "7fbcd725-fb37-4be8-b929-280981454365",
      "name": "Check Azure OpenAI Health",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1824,
        1768
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.azureHealthEndpoint }}",
        "options": {
          "timeout": 3000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "allowUnauthorizedCerts": true
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "5cbf7f8d-e33f-4fbf-8976-ce5b3f8f160c",
      "name": "Check AWS Bedrock Health",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1824,
        1960
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.awsHealthEndpoint }}",
        "options": {
          "timeout": 3000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "a5e5565e-9668-4619-a8f1-7d45b5b72da4",
      "name": "Merge Health Checks",
      "type": "n8n-nodes-base.merge",
      "position": [
        2048,
        1960
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "9155f3f1-8949-4b2d-b2c1-0694e25dec3d",
      "name": "Score & Rank Models",
      "type": "n8n-nodes-base.code",
      "position": [
        2272,
        1960
      ],
      "parameters": {
        "jsCode": "// Score & Rank Models based on multiple criteria\n// Inputs: health checks, historical performance, prompt complexity, config\n\nconst items = $input.all();\n\n// Extract data from merged inputs\nconst healthChecks = [];\nconst historicalPerformance = [];\nlet promptComplexity = {};\nlet config = {};\n\n// Parse input items to separate health checks, historical data, and config\nfor (const item of items) {\n  if (item.json.type === 'health_check') {\n    healthChecks.push(item.json);\n  } else if (item.json.type === 'historical_performance') {\n    historicalPerformance.push(item.json);\n  } else if (item.json.type === 'prompt_complexity') {\n    promptComplexity = item.json;\n  } else if (item.json.type === 'config') {\n    config = item.json;\n  }\n}\n\n// Default configuration values\nconst costWeight = config.costWeight || 0.25;\nconst latencyWeight = config.latencyWeight || 0.30;\nconst qualityWeight = config.qualityWeight || 0.30;\nconst healthWeight = config.healthWeight || 0.15;\n\nconst maxCostPerRequest = config.maxCostPerRequest || 0.10;\nconst maxLatencySLO = config.maxLatencySLO || 5000; // ms\nconst minQualityScore = config.minQualityScore || 0.7;\n\n// Model definitions with base characteristics\nconst modelCharacteristics = {\n  'azure_openai': {\n    baseCost: 0.03,\n    baseLatency: 2000,\n    baseQuality: 0.92,\n    complexityMultiplier: 1.2\n  },\n  'aws_bedrock': {\n    baseCost: 0.025,\n    baseLatency: 1800,\n    baseQuality: 0.90,\n    complexityMultiplier: 1.15\n  },\n  'google_vertex': {\n    baseCost: 0.028,\n    baseLatency: 2200,\n    baseQuality: 0.91,\n    complexityMultiplier: 1.18\n  },\n  'local_model': {\n    baseCost: 0.001,\n    baseLatency: 3500,\n    baseQuality: 0.75,\n    complexityMultiplier: 1.5\n  }\n};\n\n// Function to calculate model score\nfunction calculateModelScore(modelName, health, historical, complexity) {\n  const characteristics = modelCharacteristics[modelName];\n  if (!characteristics) return 0;\n\n  // Health score (0-1)\n  const healthScore = health.status === 'healthy' ? 1.0 : \n                      health.status === 'degraded' ? 0.5 : 0.0;\n\n  // Cost score (inverse - lower cost is better)\n  const estimatedCost = characteristics.baseCost * (complexity.score || 1);\n  const costScore = Math.max(0, 1 - (estimatedCost / maxCostPerRequest));\n\n  // Latency score (inverse - lower latency is better)\n  const estimatedLatency = characteristics.baseLatency * \n                          (complexity.score || 1) * \n                          characteristics.complexityMultiplier;\n  const latencyScore = Math.max(0, 1 - (estimatedLatency / maxLatencySLO));\n\n  // Quality score (adjusted by historical performance)\n  const historicalQuality = historical?.averageQuality || characteristics.baseQuality;\n  const qualityScore = Math.min(1, historicalQuality / minQualityScore);\n\n  // Weighted total score\n  const totalScore = (\n    (healthScore * healthWeight) +\n    (costScore * costWeight) +\n    (latencyScore * latencyWeight) +\n    (qualityScore * qualityWeight)\n  );\n\n  return {\n    modelName,\n    totalScore: Math.round(totalScore * 1000) / 1000,\n    breakdown: {\n      healthScore: Math.round(healthScore * 1000) / 1000,\n      costScore: Math.round(costScore * 1000) / 1000,\n      latencyScore: Math.round(latencyScore * 1000) / 1000,\n      qualityScore: Math.round(qualityScore * 1000) / 1000\n    },\n    estimates: {\n      cost: Math.round(estimatedCost * 10000) / 10000,\n      latency: Math.round(estimatedLatency),\n      quality: Math.round(historicalQuality * 1000) / 1000\n    },\n    health: health.status,\n    meetsConstraints: (\n      estimatedCost <= maxCostPerRequest &&\n      estimatedLatency <= maxLatencySLO &&\n      historicalQuality >= minQualityScore\n    )\n  };\n}\n\n// Score all models\nconst modelScores = [];\n\nfor (const health of healthChecks) {\n  const modelName = health.model;\n  const historical = historicalPerformance.find(h => h.model === modelName);\n  \n  const score = calculateModelScore(modelName, health, historical, promptComplexity);\n  modelScores.push(score);\n}\n\n// Sort by total score (descending)\nmodelScores.sort((a, b) => b.totalScore - a.totalScore);\n\n// Add rank\nmodelScores.forEach((score, index) => {\n  score.rank = index + 1;\n});\n\n// Filter to only models that meet constraints\nconst eligibleModels = modelScores.filter(m => m.meetsConstraints && m.health !== 'unhealthy');\nconst ineligibleModels = modelScores.filter(m => !m.meetsConstraints || m.health === 'unhealthy');\n\n// Return results\nreturn [{\n  json: {\n    type: 'model_ranking',\n    timestamp: new Date().toISOString(),\n    promptComplexity: promptComplexity.score || 1,\n    constraints: {\n      maxCostPerRequest,\n      maxLatencySLO,\n      minQualityScore\n    },\n    weights: {\n      cost: costWeight,\n      latency: latencyWeight,\n      quality: qualityWeight,\n      health: healthWeight\n    },\n    rankedModels: modelScores,\n    eligibleModels,\n    ineligibleModels,\n    recommendedModel: eligibleModels.length > 0 ? eligibleModels[0].modelName : null,\n    totalModelsEvaluated: modelScores.length,\n    eligibleCount: eligibleModels.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d7a2c5ec-9580-4fc2-8112-8a070c53f8e1",
      "name": "Check Policy Constraints",
      "type": "n8n-nodes-base.if",
      "position": [
        2496,
        1960
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "exists"
              },
              "leftValue": "={{ $('Score & Rank Models').item.json.dataResidency }}"
            },
            {
              "id": "id-2",
              "operator": {
                "type": "string",
                "operation": "exists"
              },
              "leftValue": "={{ $('Score & Rank Models').item.json.complianceRequirements }}"
            },
            {
              "id": "id-3",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $('Score & Rank Models').item.json.sensitiveDataFlag }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "23dba12b-34ca-4e3b-be73-c23628002ec6",
      "name": "Apply Policy Routing",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        1816
      ],
      "parameters": {
        "jsCode": "// Apply Policy Routing - Filter and re-rank models based on regulatory constraints\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const rankedModels = item.json.rankedModels || [];\n  const promptAnalysis = item.json.promptAnalysis || {};\n  const policyConstraints = item.json.policyConstraints || {};\n  \n  // Extract sensitivity flags\n  const containsPII = promptAnalysis.containsPII || false;\n  const containsPHI = promptAnalysis.containsPHI || false;\n  const containsFinancial = promptAnalysis.containsFinancial || false;\n  const dataClassification = promptAnalysis.dataClassification || 'public';\n  \n  // Define regulatory requirements for each model\n  const modelCompliance = {\n    'azure-openai': {\n      hipaaCompliant: true,\n      gdprCompliant: true,\n      soc2Certified: true,\n      dataResidency: ['US', 'EU'],\n      piiHandling: true\n    },\n    'aws-bedrock': {\n      hipaaCompliant: true,\n      gdprCompliant: true,\n      soc2Certified: true,\n      dataResidency: ['US', 'EU', 'APAC'],\n      piiHandling: true\n    },\n    'google-vertex': {\n      hipaaCompliant: true,\n      gdprCompliant: true,\n      soc2Certified: true,\n      dataResidency: ['US', 'EU'],\n      piiHandling: true\n    },\n    'local-model': {\n      hipaaCompliant: false,\n      gdprCompliant: false,\n      soc2Certified: false,\n      dataResidency: ['on-premise'],\n      piiHandling: false\n    }\n  };\n  \n  // Filter models based on policy constraints\n  let filteredModels = rankedModels.filter(model => {\n    const compliance = modelCompliance[model.provider] || {};\n    \n    // Check HIPAA compliance for PHI data\n    if (containsPHI && !compliance.hipaaCompliant) {\n      console.log(`Filtering out ${model.provider}: Not HIPAA compliant`);\n      return false;\n    }\n    \n    // Check GDPR compliance for PII data\n    if (containsPII && policyConstraints.requireGDPR && !compliance.gdprCompliant) {\n      console.log(`Filtering out ${model.provider}: Not GDPR compliant`);\n      return false;\n    }\n    \n    // Check data residency requirements\n    if (policyConstraints.dataResidency && !compliance.dataResidency.includes(policyConstraints.dataResidency)) {\n      console.log(`Filtering out ${model.provider}: Data residency requirement not met`);\n      return false;\n    }\n    \n    // Check PII handling capability\n    if ((containsPII || containsPHI) && !compliance.piiHandling) {\n      console.log(`Filtering out ${model.provider}: Cannot handle PII/PHI data`);\n      return false;\n    }\n    \n    // Check classification level restrictions\n    if (dataClassification === 'confidential' && model.provider === 'local-model') {\n      console.log(`Filtering out ${model.provider}: Not approved for confidential data`);\n      return false;\n    }\n    \n    return true;\n  });\n  \n  // Re-rank filtered models with compliance bonus\n  filteredModels = filteredModels.map(model => {\n    const compliance = modelCompliance[model.provider] || {};\n    let complianceBonus = 0;\n    \n    // Add bonus points for compliance features\n    if (compliance.hipaaCompliant) complianceBonus += 5;\n    if (compliance.gdprCompliant) complianceBonus += 5;\n    if (compliance.soc2Certified) complianceBonus += 3;\n    \n    // Adjust score with compliance bonus\n    const adjustedScore = (model.score || 0) + complianceBonus;\n    \n    return {\n      ...model,\n      complianceScore: complianceBonus,\n      originalScore: model.score,\n      score: adjustedScore,\n      complianceDetails: compliance\n    };\n  });\n  \n  // Sort by adjusted score (descending)\n  filteredModels.sort((a, b) => b.score - a.score);\n  \n  // Select the top model after policy filtering\n  const selectedModel = filteredModels.length > 0 ? filteredModels[0] : null;\n  \n  if (!selectedModel) {\n    throw new Error('No models available that meet policy constraints');\n  }\n  \n  console.log(`Policy routing selected: ${selectedModel.provider} (score: ${selectedModel.score})`);\n  \n  results.push({\n    json: {\n      ...item.json,\n      filteredModels,\n      selectedModel,\n      policyApplied: true,\n      modelsFiltered: rankedModels.length - filteredModels.length,\n      policyReason: {\n        containsPII,\n        containsPHI,\n        containsFinancial,\n        dataClassification,\n        constraints: policyConstraints\n      }\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "8a0c5e8e-9566-4a62-a7bf-4ea58cb7a2e2",
      "name": "Route to Selected Model",
      "type": "n8n-nodes-base.switch",
      "position": [
        2944,
        1912
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Azure",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.selectedModel }}",
                    "rightValue": "azure"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "AWS",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.selectedModel }}",
                    "rightValue": "aws"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Google",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.selectedModel }}",
                    "rightValue": "google"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Local",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.selectedModel }}",
                    "rightValue": "local"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Fallback"
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cf08c853-68c4-4af4-8a5a-20e198b7bfdf",
      "name": "Rewrite Prompt for Azure",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        1480
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Rewrite and optimize prompt for Azure OpenAI format\nconst item = $input.item.json;\n\n// Extract prompt and configuration from previous nodes\nconst userPrompt = item.prompt || item.userPrompt || item.message || '';\nconst complexity = item.complexity || 'medium';\nconst selectedModel = item.selectedModel || 'gpt-4';\nconst maxTokens = item.maxTokens || 2000;\nconst temperature = item.temperature || 0.7;\n\n// Build Azure OpenAI compatible request format\nconst azureRequest = {\n  messages: [\n    {\n      role: 'system',\n      content: 'You are a helpful AI assistant. Provide accurate, concise, and well-structured responses.'\n    },\n    {\n      role: 'user',\n      content: userPrompt\n    }\n  ],\n  temperature: temperature,\n  max_tokens: maxTokens,\n  top_p: 0.95,\n  frequency_penalty: 0,\n  presence_penalty: 0,\n  stop: null\n};\n\n// Adjust parameters based on complexity\nif (complexity === 'high') {\n  azureRequest.temperature = Math.min(temperature, 0.5);\n  azureRequest.max_tokens = Math.max(maxTokens, 3000);\n} else if (complexity === 'low') {\n  azureRequest.temperature = Math.min(temperature + 0.2, 1.0);\n  azureRequest.max_tokens = Math.min(maxTokens, 1000);\n}\n\n// Add model-specific optimizations\nif (selectedModel.includes('gpt-4')) {\n  azureRequest.messages[0].content += ' Use your advanced reasoning capabilities to provide detailed analysis.';\n} else if (selectedModel.includes('gpt-3.5')) {\n  azureRequest.messages[0].content += ' Provide clear and efficient responses.';\n}\n\n// Return the formatted request\nreturn {\n  json: {\n    ...item,\n    azureRequest: azureRequest,\n    provider: 'azure',\n    formattedPrompt: userPrompt,\n    systemMessage: azureRequest.messages[0].content,\n    rewrittenAt: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "e83198a9-a2c4-43fe-a54a-95df1d07be6e",
      "name": "Rewrite Prompt for AWS",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        1720
      ],
      "parameters": {
        "jsCode": "// Rewrite and optimize prompt for AWS Bedrock format\nconst items = $input.all();\nconst outputItems = [];\n\nfor (const item of items) {\n  const originalPrompt = item.json.prompt || item.json.userPrompt || '';\n  const complexity = item.json.complexity || 'medium';\n  const selectedModel = item.json.selectedModel || 'anthropic.claude-v2';\n  const maxTokens = item.json.maxTokens || 2048;\n  const temperature = item.json.temperature || 0.7;\n  \n  // Extract model provider from model ID\n  const modelProvider = selectedModel.split('.')[0];\n  \n  // Rewrite prompt based on AWS Bedrock best practices\n  let rewrittenPrompt = originalPrompt;\n  \n  // Add system context for better results\n  let systemPrompt = 'You are a helpful AI assistant. Provide accurate, concise, and relevant responses.';\n  \n  // Adjust based on complexity\n  if (complexity === 'high') {\n    systemPrompt += ' Take your time to think through complex problems step by step.';\n  }\n  \n  // Format request body based on model provider\n  let bedrockRequestBody = {};\n  \n  if (modelProvider === 'anthropic') {\n    // Claude format\n    bedrockRequestBody = {\n      prompt: `\\n\\nHuman: ${rewrittenPrompt}\\n\\nAssistant:`,\n      max_tokens_to_sample: maxTokens,\n      temperature: temperature,\n      top_p: 0.9,\n      top_k: 250,\n      stop_sequences: ['\\n\\nHuman:']\n    };\n  } else if (modelProvider === 'ai21') {\n    // AI21 Jurassic format\n    bedrockRequestBody = {\n      prompt: rewrittenPrompt,\n      maxTokens: maxTokens,\n      temperature: temperature,\n      topP: 0.9,\n      stopSequences: [],\n      countPenalty: {\n        scale: 0\n      },\n      presencePenalty: {\n        scale: 0\n      },\n      frequencyPenalty: {\n        scale: 0\n      }\n    };\n  } else if (modelProvider === 'amazon') {\n    // Amazon Titan format\n    bedrockRequestBody = {\n      inputText: rewrittenPrompt,\n      textGenerationConfig: {\n        maxTokenCount: maxTokens,\n        temperature: temperature,\n        topP: 0.9,\n        stopSequences: []\n      }\n    };\n  } else if (modelProvider === 'cohere') {\n    // Cohere format\n    bedrockRequestBody = {\n      prompt: rewrittenPrompt,\n      max_tokens: maxTokens,\n      temperature: temperature,\n      p: 0.9,\n      k: 0,\n      stop_sequences: []\n    };\n  } else {\n    // Default format\n    bedrockRequestBody = {\n      prompt: rewrittenPrompt,\n      max_tokens: maxTokens,\n      temperature: temperature\n    };\n  }\n  \n  outputItems.push({\n    json: {\n      ...item.json,\n      rewrittenPrompt: rewrittenPrompt,\n      systemPrompt: systemPrompt,\n      bedrockModelId: selectedModel,\n      bedrockRequestBody: bedrockRequestBody,\n      bedrockRequestBodyString: JSON.stringify(bedrockRequestBody),\n      provider: 'aws_bedrock',\n      modelProvider: modelProvider,\n      inferenceConfig: {\n        maxTokens: maxTokens,\n        temperature: temperature,\n        topP: 0.9\n      }\n    }\n  });\n}\n\nreturn outputItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "71106353-66c9-4ac6-a0c2-6fb703afa9bf",
      "name": "Rewrite Prompt for Google",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        1912
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Rewrite and optimize prompt for Google Vertex AI format\nconst item = $input.item.json;\n\n// Extract prompt and configuration from previous nodes\nconst originalPrompt = item.prompt || item.userPrompt || item.query || '';\nconst complexity = item.complexity || 'medium';\nconst selectedModel = item.selectedModel || 'gemini-pro';\nconst maxTokens = item.maxTokens || 2048;\nconst temperature = item.temperature || 0.7;\n\n// Map complexity to Vertex AI parameters\nconst complexityConfig = {\n  low: { temperature: 0.3, topP: 0.8, topK: 20 },\n  medium: { temperature: 0.7, topP: 0.9, topK: 40 },\n  high: { temperature: 0.9, topP: 0.95, topK: 60 }\n};\n\nconst config = complexityConfig[complexity] || complexityConfig.medium;\n\n// Optimize prompt for Vertex AI (Gemini models prefer clear, structured prompts)\nlet optimizedPrompt = originalPrompt;\n\n// Add context markers for better Gemini understanding\nif (!optimizedPrompt.includes('Context:') && !optimizedPrompt.includes('Task:')) {\n  optimizedPrompt = `Task: ${optimizedPrompt}\\n\\nPlease provide a clear, accurate, and well-structured response.`;\n}\n\n// Build Vertex AI request payload\nconst vertexPayload = {\n  instances: [\n    {\n      content: optimizedPrompt\n    }\n  ],\n  parameters: {\n    temperature: temperature || config.temperature,\n    maxOutputTokens: maxTokens,\n    topP: config.topP,\n    topK: config.topK,\n    // Safety settings for Vertex AI\n    safetySettings: [\n      {\n        category: 'HARM_CATEGORY_HATE_SPEECH',\n        threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n      },\n      {\n        category: 'HARM_CATEGORY_DANGEROUS_CONTENT',\n        threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n      },\n      {\n        category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',\n        threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n      },\n      {\n        category: 'HARM_CATEGORY_HARASSMENT',\n        threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n      }\n    ]\n  }\n};\n\n// Return optimized payload for Google Vertex AI\nreturn {\n  json: {\n    ...item,\n    originalPrompt: originalPrompt,\n    optimizedPrompt: optimizedPrompt,\n    vertexPayload: vertexPayload,\n    provider: 'google-vertex',\n    model: selectedModel,\n    rewriteTimestamp: new Date().toISOString(),\n    promptOptimizations: [\n      'Added task structure',\n      'Applied Vertex AI safety settings',\n      'Configured generation parameters',\n      'Set complexity-based tuning'\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "93ff3a10-55ef-4055-bf2c-90cead9e5e5f",
      "name": "Rewrite Prompt for Local",
      "type": "n8n-nodes-base.code",
      "position": [
        3168,
        2104
      ],
      "parameters": {
        "jsCode": "// Rewrite and optimize prompt for local model format\n// Adapts the prompt to the specific local model API structure\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const originalPrompt = item.json.prompt || '';\n  const modelConfig = item.json.selectedModel || {};\n  const complexity = item.json.complexity || {};\n  const metadata = item.json.metadata || {};\n  \n  // Extract local model specific configuration\n  const localModelName = modelConfig.localModelName || 'llama2';\n  const maxTokens = modelConfig.maxTokens || 2048;\n  const temperature = modelConfig.temperature || 0.7;\n  \n  // Optimize prompt for local model\n  let optimizedPrompt = originalPrompt;\n  \n  // Add system context if needed for local models\n  const systemContext = metadata.systemContext || '';\n  if (systemContext) {\n    optimizedPrompt = `System: ${systemContext}\\n\\nUser: ${originalPrompt}`;\n  }\n  \n  // Format for common local model APIs (Ollama, LM Studio, etc.)\n  const localModelRequest = {\n    model: localModelName,\n    prompt: optimizedPrompt,\n    options: {\n      temperature: temperature,\n      num_predict: maxTokens,\n      top_p: 0.9,\n      top_k: 40,\n      repeat_penalty: 1.1\n    },\n    stream: false\n  };\n  \n  // Add complexity-based optimizations\n  if (complexity.level === 'high') {\n    localModelRequest.options.temperature = Math.min(temperature + 0.1, 1.0);\n    localModelRequest.options.num_predict = Math.min(maxTokens * 1.5, 4096);\n  }\n  \n  results.push({\n    json: {\n      ...item.json,\n      rewrittenPrompt: optimizedPrompt,\n      localModelRequest: localModelRequest,\n      originalPrompt: originalPrompt,\n      modelProvider: 'local',\n      rewriteTimestamp: new Date().toISOString()\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "92217e1d-c378-4b3a-a42e-c8cfaa9e46db",
      "name": "Call Azure OpenAI",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3392,
        1480
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.azureApiEndpoint }}",
        "body": "={{ JSON.stringify($json.requestBody) }}",
        "method": "POST",
        "options": {
          "timeout": "={{ $json.latencySLO || 30000 }}"
        },
        "sendBody": true,
        "contentType": "raw",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "rawContentType": "application/json",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "c6388998-9adc-4628-b474-41a5a04be133",
      "name": "Call AWS Bedrock",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3392,
        1720
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.awsApiEndpoint }}",
        "body": "={{ JSON.stringify($json.requestBody) }}",
        "method": "POST",
        "options": {
          "timeout": "={{ $json.latencySLO || 30000 }}"
        },
        "sendBody": true,
        "contentType": "raw",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "rawContentType": "application/json",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "e92653ff-bfbe-4fd3-907f-1152cefae06d",
      "name": "Call Google Vertex AI",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3392,
        1912
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.googleApiEndpoint }}",
        "body": "={{ JSON.stringify($json.requestBody) }}",
        "method": "POST",
        "options": {
          "timeout": "={{ $json.latencySLO || 30000 }}"
        },
        "sendBody": true,
        "contentType": "raw",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "rawContentType": "application/json",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "0842cfa1-25bc-481e-a10e-ad7f6f31e6cc",
      "name": "Call Local Model",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3392,
        2104
      ],
      "parameters": {
        "url": "={{ $('Workflow Configuration').first().json.localApiEndpoint }}",
        "body": "={{ JSON.stringify($json.requestBody) }}",
        "method": "POST",
        "options": {
          "timeout": "={{ $json.latencySLO || 30000 }}"
        },
        "sendBody": true,
        "contentType": "raw",
        "sendHeaders": true,
        "rawContentType": "application/json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "aaf3d78d-a12f-4a80-b3b0-4f2e5f43f86e",
      "name": "Handle Circuit Breaker",
      "type": "n8n-nodes-base.code",
      "position": [
        3616,
        1408
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Circuit Breaker Logic for Multi-Cloud LLM Orchestration\n// Tracks failure counts per model, opens circuit after threshold, determines retry/fallback\n\nconst item = $input.item.json;\n\n// Configuration\nconst FAILURE_THRESHOLD = 3;\nconst CIRCUIT_OPEN_DURATION_MS = 60000; // 1 minute\nconst MAX_RETRIES = 2;\n\n// Initialize circuit breaker state (in production, use external state store)\nconst circuitState = $('Workflow Configuration').item.json.circuitState || {};\n\n// Extract model information and response status\nconst modelName = item.selectedModel || item.model || 'unknown';\nconst isSuccess = item.statusCode >= 200 && item.statusCode < 300;\nconst responseTime = item.responseTime || 0;\nconst errorMessage = item.error || '';\n\n// Initialize model state if not exists\nif (!circuitState[modelName]) {\n  circuitState[modelName] = {\n    failureCount: 0,\n    lastFailureTime: null,\n    circuitStatus: 'closed', // closed, open, half-open\n    consecutiveSuccesses: 0,\n    totalRequests: 0,\n    totalFailures: 0\n  };\n}\n\nconst modelState = circuitState[modelName];\nmodelState.totalRequests++;\n\n// Check current circuit status\nlet shouldRetry = false;\nlet shouldFallback = false;\nlet retryCount = item.retryCount || 0;\n\n// If circuit is open, check if enough time has passed to try half-open\nif (modelState.circuitStatus === 'open') {\n  const timeSinceFailure = Date.now() - modelState.lastFailureTime;\n  \n  if (timeSinceFailure >= CIRCUIT_OPEN_DURATION_MS) {\n    // Move to half-open state for testing\n    modelState.circuitStatus = 'half-open';\n    console.log(`Circuit for ${modelName} moved to HALF-OPEN state`);\n  } else {\n    // Circuit still open, trigger immediate fallback\n    shouldFallback = true;\n    console.log(`Circuit for ${modelName} is OPEN. Triggering fallback.`);\n  }\n}\n\n// Process current request result\nif (isSuccess) {\n  // Success handling\n  modelState.consecutiveSuccesses++;\n  \n  if (modelState.circuitStatus === 'half-open' && modelState.consecutiveSuccesses >= 2) {\n    // Close circuit after successful half-open tests\n    modelState.circuitStatus = 'closed';\n    modelState.failureCount = 0;\n    console.log(`Circuit for ${modelName} CLOSED after successful recovery`);\n  } else if (modelState.circuitStatus === 'closed') {\n    // Reset failure count on success in closed state\n    modelState.failureCount = 0;\n  }\n  \n  shouldRetry = false;\n  shouldFallback = false;\n  \n} else {\n  // Failure handling\n  modelState.failureCount++;\n  modelState.totalFailures++;\n  modelState.lastFailureTime = Date.now();\n  modelState.consecutiveSuccesses = 0;\n  \n  console.log(`Failure detected for ${modelName}. Count: ${modelState.failureCount}/${FAILURE_THRESHOLD}`);\n  \n  // Check if we should open the circuit\n  if (modelState.failureCount >= FAILURE_THRESHOLD) {\n    modelState.circuitStatus = 'open';\n    console.log(`Circuit for ${modelName} OPENED due to ${FAILURE_THRESHOLD} consecutive failures`);\n    shouldFallback = true;\n    shouldRetry = false;\n  } else if (retryCount < MAX_RETRIES) {\n    // Still within retry limits and circuit not open\n    shouldRetry = true;\n    shouldFallback = false;\n    retryCount++;\n  } else {\n    // Exceeded retry limit\n    shouldFallback = true;\n    shouldRetry = false;\n    console.log(`Max retries (${MAX_RETRIES}) exceeded for ${modelName}. Triggering fallback.`);\n  }\n}\n\n// Calculate failure rate\nconst failureRate = modelState.totalRequests > 0 \n  ? (modelState.totalFailures / modelState.totalRequests * 100).toFixed(2)\n  : 0;\n\n// Prepare output\nreturn {\n  json: {\n    ...item,\n    circuitBreaker: {\n      modelName,\n      circuitStatus: modelState.circuitStatus,\n      failureCount: modelState.failureCount,\n      failureThreshold: FAILURE_THRESHOLD,\n      consecutiveSuccesses: modelState.consecutiveSuccesses,\n      totalRequests: modelState.totalRequests,\n      totalFailures: modelState.totalFailures,\n      failureRate: `${failureRate}%`,\n      lastFailureTime: modelState.lastFailureTime,\n      shouldRetry,\n      shouldFallback,\n      retryCount,\n      maxRetries: MAX_RETRIES,\n      timestamp: Date.now()\n    },\n    shouldRetry,\n    shouldFallback,\n    retryCount,\n    circuitState // Pass updated state forward\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "003a5db0-1db5-4593-aa66-5bb6bc1edda5",
      "name": "Check Retry Needed",
      "type": "n8n-nodes-base.if",
      "position": [
        3840,
        1408
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $('Handle Circuit Breaker').item.json.shouldRetry }}"
            },
            {
              "id": "id-2",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $('Handle Circuit Breaker').item.json.retryCount }}",
              "rightValue": "={{ $('Handle Circuit Breaker').item.json.maxRetries }}"
            },
            {
              "id": "id-3",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $('Handle Circuit Breaker').item.json.circuitBreakerState }}",
              "rightValue": "open"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "6d1bd635-cd97-4e8a-aeb2-bf7bf33e4de6",
      "name": "Adaptive Retry Delay",
      "type": "n8n-nodes-base.wait",
      "position": [
        4064,
        1552
      ],
      "parameters": {
        "amount": "={{ $('Workflow Configuration').first().json.retryBaseDelay * Math.pow(2, $json.retryCount || 0) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "f24bc275-47d1-498b-82b1-4bfabb76907e",
      "name": "Merge Model Responses",
      "type": "n8n-nodes-base.merge",
      "position": [
        4064,
        1768
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "4cf06d1b-698e-4f9f-9dbd-c52083dd5da9",
      "name": "Normalize Output",
      "type": "n8n-nodes-base.code",
      "position": [
        4288,
        1768
      ],
      "parameters": {
        "jsCode": "// Normalize model outputs into a consistent format\n// Extract text, metadata, and provider-specific fields into a unified structure\n\nconst items = $input.all();\nconst normalizedOutputs = [];\n\nfor (const item of items) {\n  const rawResponse = item.json;\n  \n  // Initialize normalized structure\n  const normalized = {\n    text: '',\n    provider: '',\n    model: '',\n    metadata: {\n      tokensUsed: 0,\n      promptTokens: 0,\n      completionTokens: 0,\n      latencyMs: 0,\n      finishReason: '',\n      requestId: ''\n    },\n    providerSpecific: {},\n    timestamp: new Date().toISOString(),\n    success: true\n  };\n  \n  // Detect provider and normalize accordingly\n  if (rawResponse.provider === 'azure' || rawResponse.choices) {\n    // Azure OpenAI format\n    normalized.provider = 'azure';\n    normalized.text = rawResponse.choices?.[0]?.message?.content || rawResponse.choices?.[0]?.text || '';\n    normalized.model = rawResponse.model || 'gpt-4';\n    normalized.metadata.tokensUsed = rawResponse.usage?.total_tokens || 0;\n    normalized.metadata.promptTokens = rawResponse.usage?.prompt_tokens || 0;\n    normalized.metadata.completionTokens = rawResponse.usage?.completion_tokens || 0;\n    normalized.metadata.finishReason = rawResponse.choices?.[0]?.finish_reason || '';\n    normalized.metadata.requestId = rawResponse.id || '';\n    normalized.providerSpecific = {\n      systemFingerprint: rawResponse.system_fingerprint,\n      created: rawResponse.created\n    };\n  } \n  else if (rawResponse.provider === 'aws' || rawResponse.body) {\n    // AWS Bedrock format\n    normalized.provider = 'aws';\n    const body = typeof rawResponse.body === 'string' ? JSON.parse(rawResponse.body) : rawResponse.body;\n    normalized.text = body.completion || body.results?.[0]?.outputText || '';\n    normalized.model = rawResponse.modelId || 'claude-v2';\n    normalized.metadata.tokensUsed = body.usage?.total_tokens || (body.inputTextTokenCount || 0) + (body.results?.[0]?.tokenCount || 0);\n    normalized.metadata.promptTokens = body.inputTextTokenCount || 0;\n    normalized.metadata.completionTokens = body.results?.[0]?.tokenCount || 0;\n    normalized.metadata.finishReason = body.stop_reason || body.results?.[0]?.completionReason || '';\n    normalized.metadata.requestId = rawResponse.ResponseMetadata?.RequestId || '';\n    normalized.providerSpecific = {\n      stopReason: body.stop_reason,\n      amazonBedrockInvocationMetrics: rawResponse.ResponseMetadata?.HTTPHeaders?.['x-amzn-bedrock-invocation-latency']\n    };\n  }\n  else if (rawResponse.provider === 'google' || rawResponse.predictions) {\n    // Google Vertex AI format\n    normalized.provider = 'google';\n    normalized.text = rawResponse.predictions?.[0]?.content || rawResponse.predictions?.[0]?.candidates?.[0]?.content || '';\n    normalized.model = rawResponse.deployedModelId || 'text-bison';\n    normalized.metadata.tokensUsed = rawResponse.metadata?.tokenMetadata?.totalTokens || 0;\n    normalized.metadata.promptTokens = rawResponse.metadata?.tokenMetadata?.inputTokens || 0;\n    normalized.metadata.completionTokens = rawResponse.metadata?.tokenMetadata?.outputTokens || 0;\n    normalized.metadata.finishReason = rawResponse.predictions?.[0]?.safetyAttributes?.blocked ? 'blocked' : 'stop';\n    normalized.metadata.requestId = rawResponse.metadata?.requestId || '';\n    normalized.providerSpecific = {\n      safetyAttributes: rawResponse.predictions?.[0]?.safetyAttributes,\n      citationMetadata: rawResponse.predictions?.[0]?.citationMetadata\n    };\n  }\n  else if (rawResponse.provider === 'local' || rawResponse.response) {\n    // Local model format (e.g., Ollama, LM Studio)\n    normalized.provider = 'local';\n    normalized.text = rawResponse.response || rawResponse.text || rawResponse.output || '';\n    normalized.model = rawResponse.model || 'local-llm';\n    normalized.metadata.tokensUsed = rawResponse.eval_count || 0;\n    normalized.metadata.promptTokens = rawResponse.prompt_eval_count || 0;\n    normalized.metadata.completionTokens = rawResponse.eval_count || 0;\n    normalized.metadata.finishReason = rawResponse.done ? 'stop' : 'length';\n    normalized.metadata.requestId = rawResponse.created_at || '';\n    normalized.providerSpecific = {\n      totalDuration: rawResponse.total_duration,\n      loadDuration: rawResponse.load_duration,\n      evalDuration: rawResponse.eval_duration\n    };\n  }\n  else {\n    // Generic fallback\n    normalized.provider = rawResponse.provider || 'unknown';\n    normalized.text = rawResponse.text || rawResponse.content || rawResponse.output || JSON.stringify(rawResponse);\n    normalized.model = rawResponse.model || 'unknown';\n    normalized.success = false;\n  }\n  \n  // Calculate latency if available\n  if (rawResponse.startTime && rawResponse.endTime) {\n    normalized.metadata.latencyMs = rawResponse.endTime - rawResponse.startTime;\n  } else if (rawResponse.latency) {\n    normalized.metadata.latencyMs = rawResponse.latency;\n  }\n  \n  normalizedOutputs.push({ json: normalized });\n}\n\nreturn normalizedOutputs;"
      },
      "typeVersion": 2
    },
    {
      "id": "5f4baa3c-b785-4d5a-8314-9e1b87e0314c",
      "name": "Calculate Telemetry Metrics",
      "type": "n8n-nodes-base.code",
      "position": [
        4512,
        1768
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Calculate Telemetry Metrics\nconst item = $input.item.json;\n\n// Extract model response data\nconst modelResponse = item.modelResponse || {};\nconst selectedModel = item.selectedModel || 'unknown';\nconst startTime = item.startTime || Date.now();\n\n// Calculate token usage\nconst promptTokens = modelResponse.usage?.prompt_tokens || 0;\nconst completionTokens = modelResponse.usage?.completion_tokens || 0;\nconst totalTokens = promptTokens + completionTokens;\n\n// Calculate actual latency (in milliseconds)\nconst endTime = Date.now();\nconst actualLatency = endTime - startTime;\n\n// Model pricing per 1K tokens (adjust based on actual pricing)\nconst modelPricing = {\n  'azure-openai': { prompt: 0.0015, completion: 0.002 },\n  'aws-bedrock': { prompt: 0.0008, completion: 0.0024 },\n  'google-vertex': { prompt: 0.00025, completion: 0.0005 },\n  'local-model': { prompt: 0, completion: 0 }\n};\n\n// Get pricing for selected model\nconst pricing = modelPricing[selectedModel] || { prompt: 0, completion: 0 };\n\n// Calculate cost\nconst promptCost = (promptTokens / 1000) * pricing.prompt;\nconst completionCost = (completionTokens / 1000) * pricing.completion;\nconst totalCost = promptCost + completionCost;\n\n// Create telemetry metrics object\nconst telemetryMetrics = {\n  totalTokens: totalTokens,\n  promptTokens: promptTokens,\n  completionTokens: completionTokens,\n  actualLatency: actualLatency,\n  latencySeconds: (actualLatency / 1000).toFixed(3),\n  cost: {\n    promptCost: parseFloat(promptCost.toFixed(6)),\n    completionCost: parseFloat(completionCost.toFixed(6)),\n    totalCost: parseFloat(totalCost.toFixed(6)),\n    currency: 'USD'\n  },\n  timestamp: new Date().toISOString(),\n  model: selectedModel,\n  startTime: startTime,\n  endTime: endTime\n};\n\n// Return enriched item with telemetry metrics\nreturn {\n  json: {\n    ...item,\n    telemetryMetrics: telemetryMetrics\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "724e0d2f-097b-4e90-9aa0-f07cc7f1a047",
      "name": "Assess Hallucination Risk",
      "type": "n8n-nodes-base.code",
      "position": [
        4736,
        1768
      ],
      "parameters": {
        "jsCode": "// Assess Hallucination Risk\n// Analyzes LLM response for potential hallucinations using multiple heuristics\n// Produces a risk score from 0 (low risk) to 1 (high risk)\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const response = item.json.response || item.json.output || '';\n  const prompt = item.json.prompt || item.json.input || '';\n  const modelName = item.json.selectedModel || 'unknown';\n  \n  // Initialize risk factors\n  let riskScore = 0;\n  const riskFactors = [];\n  \n  // 1. Response Consistency Check\n  // Look for contradictions within the response\n  const sentences = response.split(/[.!?]+/).filter(s => s.trim().length > 0);\n  let contradictionScore = 0;\n  \n  // Check for common contradiction patterns\n  const contradictionPatterns = [\n    { pattern: /(however|but|although|despite).*?(not|never|no|none)/gi, weight: 0.1 },\n    { pattern: /(always|never|all|none).*?(sometimes|maybe|possibly)/gi, weight: 0.15 },\n    { pattern: /(yes|correct|true).*?(no|incorrect|false)/gi, weight: 0.2 }\n  ];\n  \n  for (const { pattern, weight } of contradictionPatterns) {\n    const matches = response.match(pattern);\n    if (matches && matches.length > 0) {\n      contradictionScore += weight * matches.length;\n      riskFactors.push(`Contradiction pattern detected (${matches.length} instances)`);\n    }\n  }\n  \n  riskScore += Math.min(contradictionScore, 0.3);\n  \n  // 2. Confidence Markers Analysis\n  // Lack of confidence markers or excessive hedging indicates uncertainty\n  const uncertaintyMarkers = [\n    'might', 'maybe', 'possibly', 'perhaps', 'could be', 'may be',\n    'i think', 'i believe', 'probably', 'likely', 'unclear',\n    'not sure', 'uncertain', 'approximately', 'roughly'\n  ];\n  \n  let uncertaintyCount = 0;\n  const lowerResponse = response.toLowerCase();\n  \n  for (const marker of uncertaintyMarkers) {\n    const regex = new RegExp(marker, 'gi');\n    const matches = lowerResponse.match(regex);\n    if (matches) {\n      uncertaintyCount += matches.length;\n    }\n  }\n  \n  // High uncertainty marker density suggests hallucination risk\n  const uncertaintyDensity = uncertaintyCount / Math.max(sentences.length, 1);\n  if (uncertaintyDensity > 0.3) {\n    riskScore += 0.2;\n    riskFactors.push(`High uncertainty marker density: ${uncertaintyDensity.toFixed(2)}`);\n  } else if (uncertaintyDensity > 0.15) {\n    riskScore += 0.1;\n    riskFactors.push(`Moderate uncertainty marker density: ${uncertaintyDensity.toFixed(2)}`);\n  }\n  \n  // 3. Factual Grounding Indicators\n  // Check for specific dates, numbers, citations, or references\n  const groundingPatterns = [\n    /\\b(19|20)\\d{2}\\b/g, // Years\n    /\\b\\d+(\\.\\d+)?%\\b/g, // Percentages\n    /\\b\\d+(\\.\\d+)?\\s*(million|billion|thousand)\\b/gi, // Large numbers\n    /\\b(according to|based on|research shows|studies indicate|data suggests)\\b/gi, // Citations\n    /\\b(source:|reference:|citation:)\\b/gi // Explicit references\n  ];\n  \n  let groundingScore = 0;\n  for (const pattern of groundingPatterns) {\n    const matches = response.match(pattern);\n    if (matches) {\n      groundingScore += matches.length;\n    }\n  }\n  \n  // Low grounding in factual responses increases risk\n  const groundingDensity = groundingScore / Math.max(sentences.length, 1);\n  if (groundingDensity < 0.1 && response.length > 200) {\n    riskScore += 0.15;\n    riskFactors.push('Low factual grounding indicators');\n  }\n  \n  // 4. Specificity vs Vagueness\n  // Vague responses are more likely to be hallucinated\n  const vagueTerms = [\n    'some', 'many', 'various', 'several', 'often', 'sometimes',\n    'generally', 'typically', 'usually', 'commonly', 'frequently',\n    'thing', 'stuff', 'something', 'somewhere'\n  ];\n  \n  let vaguenessCount = 0;\n  for (const term of vagueTerms) {\n    const regex = new RegExp(`\\\\b${term}\\\\b`, 'gi');\n    const matches = lowerResponse.match(regex);\n    if (matches) {\n      vaguenessCount += matches.length;\n    }\n  }\n  \n  const vaguenessDensity = vaguenessCount / Math.max(sentences.length, 1);\n  if (vaguenessDensity > 0.4) {\n    riskScore += 0.15;\n    riskFactors.push(`High vagueness density: ${vaguenessDensity.toFixed(2)}`);\n  }\n  \n  // 5. Response Length Analysis\n  // Extremely short or verbose responses can indicate issues\n  if (response.length < 50 && prompt.length > 100) {\n    riskScore += 0.1;\n    riskFactors.push('Response too

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow automates end-to-end research analysis by coordinating multiple AI models—including NVIDIA NIM (Llama), OpenAI GPT-4, and Claude to analyze uploaded documents, extract insights, and generate polished reports delivered via email. Built for researchers, academics,…

Source: https://n8n.io/workflows/12537/ — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

Advanced Workflow with Branching and Error Handling. Uses emailSend, httpRequest, postgres, slack. Webhook trigger; 12 nodes.

Email Send, HTTP Request, Postgres +1
Slack & Telegram

This n8n workflow automates task creation and scheduled reminders for users via a Telegram bot, ensuring timely notifications across multiple channels like email and Slack. It streamlines task managem

Postgres, Email Send, Slack +1
Slack & Telegram

QA Platform — Jira Story to Test Workflow. Uses jiraTrigger, postgres, httpRequest, slack. Webhook trigger; 20 nodes.

Jira Trigger, Postgres, HTTP Request +1
Slack & Telegram

This workflow provides a complete, automated post-purchase solution triggered by a successful payment webhook from Abacate Pay. (For international users, think of Abacate Pay as 'the Brazilian Stripe'

Email Send, Slack, HTTP Request
Slack & Telegram

Payment Processing Workflow. Uses postgres, hubspot, emailSend, slack. Webhook trigger; 11 nodes.

Postgres, HubSpot, Email Send +1