SHEETS RAG

工作流概述

这是一个包含23个节点的复杂工作流,主要用于自动化处理各种任务。

工作流源代码

下载
{
  "id": "7gRbzEzCuOzQKn4M",
  "meta": {
    "instanceId": "edc0464b1050024ebda3e16fceea795e4fdf67b1f61187c4f2f3a72397278df0",
    "templateCredsSetupCompleted": true
  },
  "name": "SHEETS RAG",
  "tags": [],
  "nodes": [
    {
      "id": "a073154f-53ad-45e2-9937-d0a4196c7838",
      "name": "create table query",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        2360
      ],
      "parameters": {
        "jsCode": "// Helper function to check if a string is in MM/DD/YYYY format
function isDateString(value) {
  const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/;
  if (typeof value !== 'string') return false;
  if (!dateRegex.test(value)) return false;
  const [month, day, year] = value.split('/').map(Number);
  const date = new Date(year, month - 1, day);
  return !isNaN(date.getTime());
}

const tableName = `ai_table_${$('change_this').first().json.sheet_name}`;
const rows = $('fetch sheet data').all();
const allColumns = new Set();

// Collect column names dynamically
rows.forEach(row => {
  Object.keys(row.json).forEach(col => allColumns.add(col));
});

// Ensure \"ai_table_identifier\" is always the first column
const originalColumns = [\"ai_table_identifier\", ...Array.from(allColumns)];

// Function to detect currency type (unchanged)
function detectCurrency(values) {
  const currencySymbols = {
    '₹': 'INR', '$': 'USD', '€': 'EUR', '£': 'GBP', '¥': 'JPY',
    '₩': 'KRW', '฿': 'THB', 'zł': 'PLN', 'kr': 'SEK', 'R$': 'BRL',
    'C$': 'CAD', 'A$': 'AUD'
  };

  let detectedCurrency = null;
  for (const value of values) {
    if (typeof value === 'string' && value.trim() !== '') {
      for (const [symbol, code] of Object.entries(currencySymbols)) {
        if (value.trim().startsWith(symbol)) {
          detectedCurrency = code;
          break;
        }
      }
    }
  }
  return detectedCurrency;
}

// Function to generate consistent column names
function generateColumnName(originalName, typeInfo) {
  if (typeInfo.isCurrency) {
    return `${originalName}_${typeInfo.currencyCode.toLowerCase()}`;
  }
  return originalName;
}

// Infer column types and transform names
const columnMapping = {};
originalColumns.forEach(col => {
  let typeInfo = { type: \"TEXT\" };

  if (col !== \"ai_table_identifier\") {
    const sampleValues = rows
      .map(row => row.json[col])
      .filter(value => value !== undefined && value !== null);

    // Check for currency first
    const currencyCode = detectCurrency(sampleValues);
    if (currencyCode) {
      typeInfo = { type: \"DECIMAL(15,2)\", isCurrency: true, currencyCode };
    }
    // If all sample values match MM/DD/YYYY, treat the column as a date
    else if (sampleValues.length > 0 && sampleValues.every(val => isDateString(val))) {
      typeInfo = { type: \"TIMESTAMP\" };
    }
  }

  const newColumnName = generateColumnName(col, typeInfo);
  columnMapping[col] = { newName: newColumnName, typeInfo };
});

// Final column names
const mappedColumns = originalColumns.map(col => columnMapping[col]?.newName || col);

// Define SQL columns – note that for simplicity, this example still uses TEXT for non-special types,
// but you can adjust it so that TIMESTAMP columns are created with a TIMESTAMP type.
const columnDefinitions = [`\"ai_table_identifier\" UUID PRIMARY KEY DEFAULT gen_random_uuid()`]
  .concat(mappedColumns.slice(1).map(col => {
    // If the column was inferred as TIMESTAMP, use that type in the CREATE TABLE statement.
    const originalCol = Object.keys(columnMapping).find(key => columnMapping[key].newName === col);
    const inferredType = columnMapping[originalCol]?.typeInfo?.type;
    return `\"${col}\" ${inferredType === \"TIMESTAMP\" ? \"TIMESTAMP\" : \"TEXT\"}`;
  }))
  .join(\", \");

const createTableQuery = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefinitions});`;

return [{ 
  query: createTableQuery,
  columnMapping: columnMapping 
}];
"
      },
      "typeVersion": 2
    },
    {
      "id": "2beb72c4-dab4-4058-b587-545a8ce8b86d",
      "name": "create insertion query",
      "type": "n8n-nodes-base.code",
      "position": [
        1660,
        2360
      ],
      "parameters": {
        "jsCode": "const tableName = `ai_table_${$('change_this').first().json.sheet_name}`;
const rows = $('fetch sheet data').all();
const allColumns = new Set();

// Get column mapping from previous node
const columnMapping = $('create table query').first().json.columnMapping || {};

// Collect column names dynamically
rows.forEach(row => {
  Object.keys(row.json).forEach(col => allColumns.add(col));
});

const originalColumns = Array.from(allColumns);
const mappedColumns = originalColumns.map(col => 
  columnMapping[col] ? columnMapping[col].newName : col
);

// Helper function to check if a string is a valid timestamp
function isValidTimestamp(value) {
  const date = new Date(value);
  return !isNaN(date.getTime());
}

// Helper to detect currency symbol (unchanged)
function getCurrencySymbol(value) {
  if (typeof value !== 'string') return null;
  
  const currencySymbols = ['₹', '$', '€', '£', '¥', '₩', '฿', 'zł', 'kr', 'R$', 'C$', 'A$'];
  for (const symbol of currencySymbols) {
    if (value.trim().startsWith(symbol)) {
      return symbol;
    }
  }
  return null;
}

// Helper to normalize currency values (unchanged)
function normalizeCurrencyValue(value, currencySymbol) {
  if (typeof value !== 'string') return null;
  if (!currencySymbol) return value;
  
  const numericPart = value.replace(currencySymbol, '').replace(/,/g, '');
  return !isNaN(parseFloat(numericPart)) ? parseFloat(numericPart) : null;
}

// Helper to normalize percentage values (unchanged)
function normalizePercentageValue(value) {
  if (typeof value !== 'string') return value;
  if (!value.trim().endsWith('%')) return value;
  
  const numericPart = value.replace('%', '');
  return !isNaN(parseFloat(numericPart)) ? parseFloat(numericPart) / 100 : null;
}

// Function to parse MM/DD/YYYY strings into ISO format
function parseDateString(value) {
  const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/;
  if (typeof value === 'string' && dateRegex.test(value)) {
    const [month, day, year] = value.split('/').map(Number);
    const date = new Date(year, month - 1, day);
    return !isNaN(date.getTime()) ? date.toISOString() : null;
  }
  return value;
}

// Format rows properly based on column mappings and types
const formattedRows = rows.map(row => {
  const formattedRow = {};

  originalColumns.forEach((col, index) => {
    const mappedCol = mappedColumns[index];
    let value = row.json[col];
    const typeInfo = columnMapping[col]?.typeInfo || { type: \"TEXT\" };

    if (value === \"\" || value === null || value === undefined) {
      value = null;
    } 
    else if (typeInfo.isCurrency) {
      const symbol = getCurrencySymbol(value);
      if (symbol) {
        value = normalizeCurrencyValue(value, symbol);
      } else {
        value = null;
      }
    }
    else if (typeInfo.isPercentage) {
      if (typeof value === 'string' && value.trim().endsWith('%')) {
        value = normalizePercentageValue(value);
      } else {
        value = !isNaN(parseFloat(value)) ? parseFloat(value) / 100 : null;
      }
    }
    else if (typeInfo.type === \"DECIMAL(15,2)\" || typeInfo.type === \"INTEGER\") {
      if (typeof value === 'string') {
        const cleanedValue = value.replace(/,/g, '');
        value = !isNaN(parseFloat(cleanedValue)) ? parseFloat(cleanedValue) : null;
      } else if (typeof value === 'number') {
        value = parseFloat(value);
      } else {
        value = null;
      }
    } 
    else if (typeInfo.type === \"BOOLEAN\") {
      if (typeof value === 'string') {
        const lowercased = value.toString().toLowerCase();
        value = lowercased === \"true\" ? true : 
                lowercased === \"false\" ? false : null;
      } else {
        value = Boolean(value);
      }
    } 
    else if (typeInfo.type === \"TIMESTAMP\") {
      // Check if the value is in MM/DD/YYYY format and parse it accordingly.
      if (/^\d{2}\/\d{2}\/\d{4}$/.test(value)) {
        value = parseDateString(value);
      } else if (isValidTimestamp(value)) {
        value = new Date(value).toISOString();
      } else {
        value = null;
      }
    }
    else if (typeInfo.type === \"TEXT\") {
      value = value !== null && value !== undefined ? String(value) : null;
    }

    formattedRow[mappedCol] = value;
  });

  return formattedRow;
});

// Generate SQL placeholders dynamically
const valuePlaceholders = formattedRows.map((_, rowIndex) =>
  `(${mappedColumns.map((_, colIndex) => `$${rowIndex * mappedColumns.length + colIndex + 1}`).join(\", \")})`
).join(\", \");

// Build the insert query string
const insertQuery = `INSERT INTO ${tableName} (${mappedColumns.map(col => `\"${col}\"`).join(\", \")}) VALUES ${valuePlaceholders};`;

// Flatten parameter values for PostgreSQL query
const parameters = formattedRows.flatMap(row => mappedColumns.map(col => row[col]));

return [
  {
    query: insertQuery,
    parameters: parameters
  }
];
"
      },
      "typeVersion": 2
    },
    {
      "id": "ba19c350-ffb7-4fe1-9568-2a619c914434",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        600,
        2060
      ],
      "parameters": {
        "pollTimes": {
          "item": [
            {}
          ]
        },
        "triggerOn": "specificFile",
        "fileToWatch": {
          "__rl": true,
          "mode": "list",
          "value": "1yGx4ODHYYtPV1WZFAtPcyxGT2brcXM6pl0KJhIM1f_c",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1yGx4ODHYYtPV1WZFAtPcyxGT2brcXM6pl0KJhIM1f_c/edit?usp=drivesdk",
          "cachedResultName": "Spreadsheet"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "zOt0lyWOZz1UlS67",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "dd2108fe-0cfe-453c-ac03-c0c5b10397e6",
      "name": "execute_query_tool",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        1340,
        1720
      ],
      "parameters": {
        "name": "query_executer",
        "schemaType": "manual",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "oPWJZynrMME45ks4",
          "cachedResultName": "query_executer"
        },
        "description": "Call this tool to execute a query. Remember that it should be in a postgreSQL query structure.",
        "inputSchema": "{
\"type\": \"object\",
\"properties\": {
	\"sql\": {
		\"type\": \"string\",
		\"description\": \"A SQL query based on the users question and database schema.\"
		}
	}
}",
        "specifyInputSchema": true
      },
      "typeVersion": 1.2
    },
    {
      "id": "f2c110db-1097-4b96-830d-f028e08b6713",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        880,
        1680
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.0-flash"
      },
      "credentials": {
        "googlePalmApi": {
          "id": "Kr5lNqvdmtB0Ybyo",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2460801c-5b64-41b3-93f7-4f2fbffabfd6",
      "name": "get_postgres_schema",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        1160,
        1720
      ],
      "parameters": {
        "name": "get_postgres_schema",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "iNLPk34SeRGHaeMD",
          "cachedResultName": "get database schema"
        },
        "description": "Call this tool to retrieve the schema of all the tables inside of the database. A string will be retrieved with the name of the table and its columns, each table is separated by \n\n.",
        "workflowInputs": {
          "value": {},
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2
    },
    {
      "id": "4b43ff94-df0d-40f1-9f51-cf488e33ff68",
      "name": "change_this",
      "type": "n8n-nodes-base.set",
      "position": [
        800,
        2060
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "908ed843-f848-4290-9cdb-f195d2189d7c",
              "name": "table_url",
              "type": "string",
              "value": "https://docs.google.com/spreadsheets/d/1yGx4ODHYYtPV1WZFAtPcyxGT2brcXM6pl0KJhIM1f_c/edit?gid=0#gid=0"
            },
            {
              "id": "50f8afaf-0a6c-43ee-9157-79408fe3617a",
              "name": "sheet_name",
              "type": "string",
              "value": "product_list"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a27a47ff-9328-4eef-99e8-280452cff189",
      "name": "is not in database",
      "type": "n8n-nodes-base.if",
      "position": [
        1380,
        2060
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "619ce84c-0a50-4f88-8e55-0ce529aea1fc",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $('table exists?').item.json.exists }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8ad9bc36-08b1-408e-ba20-5618a801b4ed",
      "name": "table exists?",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1000,
        2060
      ],
      "parameters": {
        "query": "SELECT EXISTS (
    SELECT 1 
    FROM information_schema.tables 
    WHERE table_name = 'ai_table_{{ $json.sheet_name }}'
);
",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "id": "KQiQIZTArTBSNJH7",
          "name": "Postgres account"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "f66b7ca7-ecb7-47fc-9214-2d2b37b0fbe4",
      "name": "fetch sheet data",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1180,
        2060
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('change_this').item.json.sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "={{ $('change_this').item.json.table_url }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "3au0rUsZErkG0zc2",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "11ba5da0-e7c4-49ee-8d35-24c8d3b9fea9",
      "name": "remove table",
      "type": "n8n-nodes-base.postgres",
      "position": [
        980,
        2360
      ],
      "parameters": {
        "query": "DROP TABLE IF EXISTS ai_table_{{ $('change_this').item.json.sheet_name }} CASCADE;",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "id": "KQiQIZTArTBSNJH7",
          "name": "Postgres account"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "3936ecb3-f084-4f86-bd5f-abab0957ebc0",
      "name": "create table",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1460,
        2360
      ],
      "parameters": {
        "query": "{{ $json.query }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "id": "KQiQIZTArTBSNJH7",
          "name": "Postgres account"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "8a3ea239-f3fa-4c72-af99-31f4bd992b58",
      "name": "perform insertion",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1860,
        2360
      ],
      "parameters": {
        "query": "{{$json.query}}",
        "options": {
          "queryReplacement": "={{$json.parameters}}"
        },
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "id": "KQiQIZTArTBSNJH7",
          "name": "Postgres account"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "21239928-b573-4753-a7ca-5a9c3aa8aa3e",
      "name": "Execute Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        1720,
        1720
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c94256a9-e44e-4800-82f8-90f85ba90bde",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        1460
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 260,
        "content": "Place this in a separate workflow named:
### query_executer"
      },
      "typeVersion": 1
    },
    {
      "id": "daec928e-58ee-43da-bd91-ba8bcd639a4a",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        1840
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 280,
        "content": "place this in a separate workflow named: 
### get database schema"
      },
      "typeVersion": 1
    },
    {
      "id": "8908e342-fcbe-4820-b623-cb95a55ea5db",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.manualChatTrigger",
      "position": [
        640,
        1540
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "d0ae90c2-169e-44d7-b3c2-4aff8e7d4be9",
      "name": "response output",
      "type": "n8n-nodes-base.set",
      "position": [
        2220,
        1540
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e2f94fb1-3deb-466a-a36c-e3476511d5f2",
              "name": "response",
              "type": "string",
              "value": "={{ $json }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "81c58d9b-ded4-4b74-8227-849e665cbdff",
      "name": "sql query executor",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2000,
        1540
      ],
      "parameters": {
        "query": "{{ $json.query.sql }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "id": "KQiQIZTArTBSNJH7",
          "name": "Postgres account"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "377d1727-4577-41bb-8656-38273fc4412b",
      "name": "schema finder",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2000,
        1920
      ],
      "parameters": {
        "query": "SELECT 
    t.schemaname,
    t.tablename,
    c.column_name,
    c.data_type
FROM 
    pg_catalog.pg_tables t
JOIN 
    information_schema.columns c
    ON t.schemaname = c.table_schema
    AND t.tablename = c.table_name
WHERE 
    t.schemaname = 'public'
ORDER BY 
    t.tablename, c.ordinal_position;",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "id": "KQiQIZTArTBSNJH7",
          "name": "Postgres account"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "89d3c59c-2b67-454d-a8f3-e90e75a28a8c",
      "name": "schema to string",
      "type": "n8n-nodes-base.code",
      "position": [
        2220,
        1920
      ],
      "parameters": {
        "jsCode": "function transformSchema(input) {
    const tables = {};
    
    input.forEach(({ json }) => {
        if (!json) return;
        
        const { tablename, schemaname, column_name, data_type } = json;
        
        if (!tables[tablename]) {
            tables[tablename] = { schema: schemaname, columns: [] };
        }
        tables[tablename].columns.push(`${column_name} (${data_type})`);
    });
    
    return Object.entries(tables)
        .map(([tablename, { schema, columns }]) => `Table ${tablename} (Schema: ${schema}) has columns: ${columns.join(\", \")}`)
        .join(\"\n\n\");
}

// Example usage
const input = $input.all();

const transformedSchema = transformSchema(input);

return { data: transformedSchema };"
      },
      "typeVersion": 2
    },
    {
      "id": "42d1b316-60ca-49db-959b-581b162ca1f9",
      "name": "AI Agent With SQL Query Prompt",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        900,
        1540
      ],
      "parameters": {
        "options": {
          "maxIterations": 5,
          "systemMessage": "=## Role
You are a **Database Query Assistant** specializing in generating PostgreSQL queries based on natural language questions. You analyze database schemas, construct appropriate SQL queries, and provide clear explanations of results.

## Tools
1. `get_postgres_schema`: Retrieves the complete database schema (tables and columns)
2. `execute_query_tool`: Executes SQL queries with the following input format:
   ```json
   {
     \"sql\": \"Your SQL query here\"
   }
   ```

## Process Flow

### 1. Analyze the Question
- Identify the **data entities** being requested (products, customers, orders, etc.)
- Determine the **query type** (COUNT, AVG, SUM, SELECT, etc.)
- Extract any **filters** or **conditions** mentioned

### 2. Fetch and Analyze Schema
- Call `get_postgres_schema` to retrieve database structure
- Identify relevant tables and columns that match the entities in the question
- Prioritize exact matches, then semantic matches

### 3. Query Construction
- Build case-insensitive queries using `LOWER(column) LIKE LOWER('%value%')`
- Filter out NULL or empty values with appropriate WHERE clauses
- Use joins when information spans multiple tables
- Apply aggregations (COUNT, SUM, AVG) as needed

### 4. Query Execution
- Execute query using the `execute_query_tool` with proper formatting
- If results require further processing, perform calculations as needed

### 5. Result Presentation
- Format results in a conversational, easy-to-understand manner
- Explain how the data was retrieved and any calculations performed
- When appropriate, suggest further questions the user might want to ask

## Best Practices
- Use parameterized queries to prevent SQL injection
- Implement proper error handling
- Respond with \"NOT_ENOUGH_INFO\" when the question lacks specificity
- Always verify table/column existence before attempting queries
- Use explicit JOINs instead of implicit joins
- Limit large result sets when appropriate

## Numeric Validation (IMPORTANT)
When validating or filtering numeric values in string columns:
1. **AVOID** complex regular expressions with `~` operator as they cause syntax errors
2. Use these safer alternatives instead:
   ```sql
   -- Simple numeric check without regex
   WHERE column_name IS NOT NULL AND trim(column_name) != '' AND column_name NOT LIKE '%[^0-9.]%'
   
   -- For type casting with validation
   WHERE column_name IS NOT NULL AND trim(column_name) != '' AND column_name ~ '[0-9]'
   
   -- Safe numeric conversion
   WHERE CASE WHEN column_name ~ '[0-9]' THEN TRUE ELSE FALSE END
   ```
3. For simple pattern matching, use LIKE instead of regex when possible
4. When CAST is needed, always guard against invalid values:
   ```sql
   SELECT SUM(CASE WHEN column_name ~ '[0-9]' THEN CAST(column_name AS NUMERIC) ELSE 0 END) AS total
   ```

## Response Structure
1. **Analysis**: Brief mention of how you understood the question
2. **Query**: The SQL statement used (in code block format)
3. **Results**: Clear presentation of the data found
4. **Explanation**: Simple description of how the data was retrieved

## Examples

### Example 1: Basic Counting Query
**Question**: \"How many products are in the inventory?\"

**Process**:
1. Analyze schema to find product/inventory tables
2. Construct a COUNT query on the relevant table
3. Execute the query
4. Present the count with context

**SQL**:
```sql
SELECT COUNT(*) AS product_count 
FROM products 
WHERE quantity IS NOT NULL;
```

**Response**:
\"There are 1,250 products currently in the inventory. This count includes all items with a non-null quantity value in the products table.\"

### Example 2: Filtered Aggregation Query
**Question**: \"What is the average order value for premium customers?\"

**Process**:
1. Identify relevant tables (orders, customers)
2. Determine join conditions
3. Apply filters for \"premium\" customers
4. Calculate average

**SQL**:
```sql
SELECT AVG(o.total_amount) AS avg_order_value
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE LOWER(c.customer_type) = LOWER('premium')
AND o.total_amount IS NOT NULL;
```

**Response**:
\"Premium customers spend an average of $85.42 per order. This was calculated by averaging the total_amount from all orders placed by customers with a 'premium' customer type.\"

### Example 3: Numeric Calculation from String Column
**Question**: \"What is the total of all ratings?\"

**Process**:
1. Find the ratings table and column
2. Use safe numeric validation
3. Sum the values

**SQL**:
```sql
SELECT SUM(CASE WHEN rating ~ '[0-9]' THEN CAST(rating AS NUMERIC) ELSE 0 END) AS total_rating
FROM ratings
WHERE rating IS NOT NULL AND trim(rating) != '';
```

**Response**:
\"The sum of all ratings is 4,285. This calculation includes all valid numeric ratings from the ratings table.\"

### Example 4: Date Range Aggregation for Revenue  
**Question**: \"How much did I make last week?\"  

**Process**:  
1. Identify the sales table and relevant columns (e.g., `sale_date` for dates and `revenue_amount` for revenue).  
2. Use PostgreSQL date functions (`date_trunc` and interval arithmetic) to calculate the date range for the previous week.  
3. Sum the revenue within the computed date range.  

**SQL**:  
```sql
SELECT SUM(revenue_amount) AS total_revenue
FROM sales_data
WHERE sale_date >= date_trunc('week', CURRENT_DATE) - INTERVAL '1 week'
  AND sale_date < date_trunc('week', CURRENT_DATE);
```  

**Response**:  
\"Last week's total revenue is calculated by summing the `revenue_amount` for records where the `sale_date` falls within the previous week. This query uses date functions to dynamically determine the correct date range.\"

Today's date: {{ $now }}"
        }
      },
      "typeVersion": 1.7
    },
    {
      "id": "368d68d0-1fe0-4dbf-9b24-ac28fd6e74c3",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        1420
      ],
      "parameters": {
        "color": 6,
        "width": 960,
        "height": 460,
        "content": "## Use a powerful LLM to correctly build the SQL queries, which will be identified from the get schema tool and then executed by the execute query tool."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "d8045db4-2852-4bbe-9b97-0d3c0acb53f7",
  "connections": {
    "change_this": {
      "main": [
        [
          {
            "node": "table exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "create table": {
      "main": [
        [
          {
            "node": "create insertion query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "remove table": {
      "main": [
        [
          {
            "node": "create table query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "schema finder": {
      "main": [
        [
          {
            "node": "schema to string",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "table exists?": {
      "main": [
        [
          {
            "node": "fetch sheet data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "fetch sheet data": {
      "main": [
        [
          {
            "node": "is not in database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "create table query": {
      "main": [
        [
          {
            "node": "create table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "execute_query_tool": {
      "ai_tool": [
        [
          {
            "node": "AI Agent With SQL Query Prompt",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "is not in database": {
      "main": [
        [
          {
            "node": "create table query",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "remove table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "sql query executor": {
      "main": [
        [
          {
            "node": "response output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_postgres_schema": {
      "ai_tool": [
        [
          {
            "node": "AI Agent With SQL Query Prompt",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "change_this",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "create insertion query": {
      "main": [
        [
          {
            "node": "perform insertion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Workflow Trigger": {
      "main": [
        [
          {
            "node": "sql query executor",
            "type": "main",
            "index": 0
          },
          {
            "node": "schema finder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent With SQL Query Prompt",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "AI Agent With SQL Query Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

功能特点

  • 自动检测新邮件
  • AI智能内容分析
  • 自定义分类规则
  • 批量处理能力
  • 详细的处理日志

技术分析

节点类型及作用

  • Code
  • Googledrivetrigger
  • @N8N/N8N Nodes Langchain.Toolworkflow
  • @N8N/N8N Nodes Langchain.Lmchatgooglegemini
  • Set

复杂度评估

配置难度:
★★★★☆
维护难度:
★★☆☆☆
扩展性:
★★★★☆

实施指南

前置条件

  • 有效的Gmail账户
  • n8n平台访问权限
  • Google API凭证
  • AI分类服务订阅

配置步骤

  1. 在n8n中导入工作流JSON文件
  2. 配置Gmail节点的认证信息
  3. 设置AI分类器的API密钥
  4. 自定义分类规则和标签映射
  5. 测试工作流执行
  6. 配置定时触发器(可选)

关键参数

参数名称 默认值 说明
maxEmails 50 单次处理的最大邮件数量
confidenceThreshold 0.8 分类置信度阈值
autoLabel true 是否自动添加标签

最佳实践

优化建议

  • 定期更新AI分类模型以提高准确性
  • 根据邮件量调整处理批次大小
  • 设置合理的分类置信度阈值
  • 定期清理过期的分类规则

安全注意事项

  • 妥善保管API密钥和认证信息
  • 限制工作流的访问权限
  • 定期审查处理日志
  • 启用双因素认证保护Gmail账户

性能优化

  • 使用增量处理减少重复工作
  • 缓存频繁访问的数据
  • 并行处理多个邮件分类任务
  • 监控系统资源使用情况

故障排除

常见问题

邮件未被正确分类

检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。

Gmail认证失败

确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。

调试技巧

  • 启用详细日志记录查看每个步骤的执行情况
  • 使用测试邮件验证分类逻辑
  • 检查网络连接和API服务状态
  • 逐步执行工作流定位问题节点

错误处理

工作流包含以下错误处理机制:

  • 网络超时自动重试(最多3次)
  • API错误记录和告警
  • 处理失败邮件的隔离机制
  • 异常情况下的回滚操作