{
  "id": "yFfwPclgdgnSPstc",
  "name": "Client Question \u2192 Instant Answer Assistant(new version)",
  "tags": [],
  "nodes": [
    {
      "id": "68866f52-49c9-4d33-9d28-fc430b61bf8b",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        5552,
        2688
      ],
      "parameters": {
        "options": {
          "responseMode": "lastNode"
        }
      },
      "typeVersion": 1.4
    },
    {
      "id": "e7302a95-1ba1-4ecc-8c16-e133ae4ac25e",
      "name": "Parse Client Message",
      "type": "n8n-nodes-base.code",
      "position": [
        5776,
        2688
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const text =\n  $json.chatInput ||\n  $json.message ||\n  $json.text ||\n  '';\n\nif (!text || !String(text).trim()) {\n  return {\n    valid: false,\n    error_stage: 'input_validation',\n    error_message: 'Empty message received. Use format: C001: Your question',\n    raw_message: text || ''\n  };\n}\n\nif (!String(text).includes(':')) {\n  return {\n    valid: false,\n    error_stage: 'input_validation',\n    error_message: 'Invalid format. Use: C001: Your question',\n    raw_message: text\n  };\n}\n\nconst parts = String(text).split(':');\nconst client_id = (parts[0] || '').trim().toUpperCase();\nconst question = parts.slice(1).join(':').trim();\n\nif (!client_id) {\n  return {\n    valid: false,\n    error_stage: 'input_validation',\n    error_message: 'Client ID is missing. Use format: C001: Your question',\n    raw_message: text\n  };\n}\n\nif (!question) {\n  return {\n    valid: false,\n    error_stage: 'input_validation',\n    error_message: 'Question is missing after client ID.',\n    raw_message: text,\n    client_id\n  };\n}\n\nreturn {\n  valid: true,\n  raw_message: text,\n  client_id,\n  question,\n  source_channel: 'chat',\n  received_at: new Date().toISOString()\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b0e7cf73-ae51-41bb-b3bc-1c87e25c7705",
      "name": "IF Valid Input?",
      "type": "n8n-nodes-base.if",
      "position": [
        6000,
        2688
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "valid_check_1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "910c9fe7-188c-4021-bd66-4bcf1a0c7e85",
      "name": "Build Invalid Input Response",
      "type": "n8n-nodes-base.code",
      "position": [
        5664,
        3056
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    status: 'failed',\n    error_stage: $json.error_stage || 'input_validation',\n    action_taken: 'Rejected invalid input',\n    client_id: $json.client_id || null,\n    question: $json.question || null,\n    raw_message: $json.raw_message || null,\n    reply: $json.error_message || 'Invalid input',\n    source_channel: $json.source_channel || 'chat',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "0550e1d5-c21d-4589-a13a-20e5dd63637a",
      "name": "Log Invalid Input",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5840,
        3056
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "client_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "client_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "question",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "question",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reply",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "reply",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "action_taken",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "action_taken",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "error_stage",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "error_stage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "source_channel",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "source_channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "interaction_logs"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM/edit?usp=drivesdk",
          "cachedResultName": "Portfolio_AI_System"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "f6271a50-6035-40d7-bf13-a3a77780e071",
      "name": "Return Invalid Response",
      "type": "n8n-nodes-base.code",
      "position": [
        6016,
        3056
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    reply: $json.reply,\n    status: $json.status,\n    error_stage: $json.error_stage\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ff7f936e-9f53-407a-9a17-c452bb454f89",
      "name": "Get Client Profile",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        6224,
        2592
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.client_id }}",
              "lookupColumn": "client_id"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM/edit#gid=0",
          "cachedResultName": "clients"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM/edit?usp=drivesdk",
          "cachedResultName": "Portfolio_AI_System"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "e45b88cb-4172-4dba-b2a9-1937a0f663b1",
      "name": "IF Client Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        6448,
        2592
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "client_found_check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ !!$json.client_id }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "7fc08ba7-2fb8-498b-bbf4-d2ab23ee62eb",
      "name": "Build Client Not Found Response",
      "type": "n8n-nodes-base.code",
      "position": [
        9712,
        2976
      ],
      "parameters": {
        "jsCode": "const parsed = $('Parse Client Message').first().json;\n\nreturn [{\n  json: {\n    status: 'failed',\n    error_stage: 'client_lookup',\n    action_taken: 'Stopped because client was not found',\n    client_id: parsed.client_id || null,\n    question: parsed.question || null,\n    reply: `Client ID ${parsed.client_id || ''} was not found in the client sheet.`,\n    source_channel: parsed.source_channel || 'chat',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f42b8d59-3998-47d9-8898-8674f32d5294",
      "name": "Get Client Holdings",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        6672,
        2496
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('Parse Client Message').first().json.client_id }}",
              "lookupColumn": "client_id"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "holdings"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM/edit?usp=drivesdk",
          "cachedResultName": "Portfolio_AI_System"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "e61cfad3-18cc-4c43-975a-72e1aebd884b",
      "name": "Prepare Symbols",
      "type": "n8n-nodes-base.code",
      "position": [
        6896,
        2496
      ],
      "parameters": {
        "jsCode": "const holdings = $input.all().map(item => item.json).filter(x => Object.keys(x).length > 0);\nconst parsed = $('Parse Client Message').first().json;\nconst client = $('Get Client Profile').first().json || {};\n\nif (!holdings.length) {\n  return [{\n    json: {\n      holdings_found: false,\n      client_id: parsed.client_id,\n      question: parsed.question,\n      client_name: client.client_name || null\n    }\n  }];\n}\n\nconst symbols = holdings\n  .map(h => (h.symbol || '').trim().toUpperCase())\n  .filter(Boolean);\n\nreturn [{\n  json: {\n    holdings_found: true,\n    holdings,\n    symbols,\n    client_id: parsed.client_id,\n    question: parsed.question,\n    client_name: client.client_name || null,\n    risk_profile: client.risk_profile || null,\n    goal: client.goal || null,\n    tone: client.tone || 'simple',\n    source_channel: parsed.source_channel || 'chat',\n    received_at: parsed.received_at\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "61e3855d-35ef-4cd2-b628-c1705e0be181",
      "name": "IF Holdings Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        7120,
        2496
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "holdings_found_check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.holdings_found }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "14ef2640-f1ab-403e-b36f-918d6f346cae",
      "name": "Build No Holdings Response",
      "type": "n8n-nodes-base.code",
      "position": [
        9712,
        2784
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nreturn [{\n  json: {\n    status: 'failed',\n    error_stage: 'holdings_lookup',\n    action_taken: 'Stopped because no holdings were found',\n    client_id: item.client_id || null,\n    question: item.question || null,\n    reply: `No holdings were found for client ${item.client_id || ''}.`,\n    source_channel: item.source_channel || 'chat',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "6149d05f-f7b1-4446-9898-47bbe02b8fbd",
      "name": "Get Live Prices",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        7344,
        2400
      ],
      "parameters": {
        "url": "https://memic-nse-quotes-api.hf.space/quotes",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "symbols",
              "value": "={{ $json.symbols }}"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "47a5734a-fb89-40af-bc6c-f84b6728d17b",
      "name": "Normalize Price Response",
      "type": "n8n-nodes-base.code",
      "position": [
        7568,
        2400
      ],
      "parameters": {
        "jsCode": "const prep = $('Prepare Symbols').first().json;\nconst api = $input.first().json || {};\n\nconst prices = api.prices || {};\nconst errors = api.errors || {};\nconst apiWorked = !!api.prices || Object.keys(api).length > 0;\n\nreturn [{\n  json: {\n    ...prep,\n    api_worked: apiWorked,\n    prices,\n    price_errors: errors,\n    raw_price_response: api\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ab88bdeb-675e-4908-9a6d-0ed70c2c1806",
      "name": "IF Price API Worked?",
      "type": "n8n-nodes-base.if",
      "position": [
        7792,
        2400
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "api_worked_check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.api_worked }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d8576673-6dd2-4ac4-a6b2-f5e36e61a722",
      "name": "Build API Failed Response",
      "type": "n8n-nodes-base.code",
      "position": [
        9712,
        2592
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nreturn [{\n  json: {\n    status: 'failed',\n    error_stage: 'market_api',\n    action_taken: 'Stopped because live price API failed',\n    client_id: item.client_id || null,\n    question: item.question || null,\n    reply: 'Live market prices could not be fetched right now. Please try again shortly.',\n    source_channel: item.source_channel || 'chat',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cd28bc55-3d8f-49e7-8b27-7884bc8af321",
      "name": "Merge Portfolio Data",
      "type": "n8n-nodes-base.code",
      "position": [
        8032,
        2352
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst holdings = data.holdings || [];\nconst prices = data.prices || {};\nconst errors = data.price_errors || {};\n\nconst merged = holdings.map(h => {\n  const symbol = (h.symbol || '').trim().toUpperCase();\n  const p = prices[symbol] || {};\n  const quantity = Number(h.quantity || 0);\n  const avgBuy = Number(h.avg_buy_price || 0);\n  const invested = quantity * avgBuy;\n\n  const hasPrice =\n    p.last_price !== undefined &&\n    p.last_price !== null &&\n    p.last_price !== '';\n\n  const ltp = hasPrice ? Number(p.last_price) : null;\n  const current_value = hasPrice ? quantity * ltp : null;\n  const pnl = hasPrice ? current_value - invested : null;\n  const pnl_pct = hasPrice && invested ? (pnl / invested) * 100 : null;\n\n  return {\n    row_number: h.row_number ?? null,\n    client_id: h.client_id,\n    asset_name: h.asset_name,\n    symbol,\n    asset_type: h.asset_type,\n    quantity,\n    avg_buy_price: avgBuy,\n    ltp,\n    invested,\n    current_value,\n    pnl,\n    pnl_pct,\n    price_available: hasPrice,\n    price_error: errors[symbol] || null\n  };\n});\n\nconst validHoldings = merged.filter(h =>\n  h.price_available &&\n  h.current_value !== null &&\n  h.pnl !== null &&\n  h.pnl_pct !== null\n);\n\nconst total_invested_all = merged.reduce((sum, h) => sum + Number(h.invested || 0), 0);\nconst total_invested_priced = validHoldings.reduce((sum, h) => sum + Number(h.invested || 0), 0);\nconst total_current_value = validHoldings.reduce((sum, h) => sum + Number(h.current_value || 0), 0);\nconst total_pnl = total_current_value - total_invested_priced;\nconst total_return_pct = total_invested_priced ? (total_pnl / total_invested_priced) * 100 : 0;\n\nconst sortedAsc = [...validHoldings].sort((a, b) => a.pnl_pct - b.pnl_pct);\nconst sortedDesc = [...validHoldings].sort((a, b) => b.pnl_pct - a.pnl_pct);\n\nconst weakest_performer = sortedAsc[0] || null;\nconst best_performer = sortedDesc[0] || null;\n\nconst missing_prices = merged\n  .filter(h => !h.price_available)\n  .map(h => ({\n    symbol: h.symbol,\n    asset_name: h.asset_name,\n    invested: h.invested,\n    reason: h.price_error || 'Price not available'\n  }));\n\nreturn [{\n  json: {\n    client_id: data.client_id,\n    client_name: data.client_name,\n    question: data.question,\n    risk_profile: data.risk_profile,\n    goal: data.goal,\n    tone: data.tone,\n    source_channel: data.source_channel,\n    received_at: data.received_at,\n    holdings: merged,\n    portfolio_summary: {\n      total_invested_all,\n      total_invested_priced,\n      total_current_value,\n      total_pnl,\n      total_return_pct,\n      priced_holdings_count: validHoldings.length,\n      missing_price_count: missing_prices.length\n    },\n    best_performer,\n    weakest_performer,\n    missing_prices\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d435ea0d-0ae1-4f66-85ba-43cbe60fdfb4",
      "name": "Get Market Context",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        8240,
        2304
      ],
      "parameters": {
        "url": "https://memic-nse-quotes-api.hf.space/summary",
        "options": {
          "timeout": 15000
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "9bfee56f-8321-423a-ab81-0c58c55f80b5",
      "name": "Attach Market Context",
      "type": "n8n-nodes-base.code",
      "position": [
        8464,
        2304
      ],
      "parameters": {
        "jsCode": "const portfolio = $('Merge Portfolio Data').first().json;\nconst ctx = $input.first().json || {};\n\nconst market_context = {\n  nifty_change_pct: ctx.nifty_change_pct ?? null,\n  sensex_change_pct: ctx.sensex_change_pct ?? null,\n  market_tone: ctx.market_tone ?? 'not available',\n  summary: ctx.summary ?? 'Market context not available'\n};\n\nreturn [{\n  json: {\n    ...portfolio,\n    market_context\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8089e345-e8a0-42f0-b284-17aa798ca3e9",
      "name": "Build AI Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        8688,
        2304
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\n          const holdingsText = (data.holdings || [])\n            .map(h => {\n              const invested = h.invested != null ? `\u20b9${Number(h.invested).toFixed(2)}` : 'N/A';\n              const ltp = h.ltp != null ? `\u20b9${Number(h.ltp).toFixed(2)}` : 'N/A';\n              const currentValue = h.current_value != null ? `\u20b9${Number(h.current_value).toFixed(2)}` : 'N/A';\n              const pnl = h.pnl != null ? `\u20b9${Number(h.pnl).toFixed(2)}` : 'N/A';\n              const pnlPct = h.pnl_pct != null ? `${Number(h.pnl_pct).toFixed(2)}%` : 'N/A';\n\n              return `- ${h.asset_name} (${h.symbol}): quantity ${h.quantity}, avg buy \u20b9${Number(h.avg_buy_price).toFixed(2)}, invested ${invested}, LTP ${ltp}, current value ${currentValue}, P&L ${pnl}, return ${pnlPct}, price available ${h.price_available ? 'yes' : 'no'}`;\n            })\n            .join('\\n');\n\n          const missingPricesText = (data.missing_prices || []).length\n            ? data.missing_prices\n                .map(x => `- ${x.asset_name} (${x.symbol}): invested \u20b9${Number(x.invested || 0).toFixed(2)}, reason: ${x.reason}`)\n                .join('\\n')\n            : 'None';\n\n          const prompt = `\nYou are a financial assistant.\n\nSTRICT RULES:\n- Use ONLY the numbers and facts provided below.\n- Never calculate new numbers on your own.\n- Never invent market reasons, company news or assumptions.\n- Never give buy/sell advice unless explicitly asked.\n- Keep the reply short, clear and client-friendly.\n- Maximum 4 short lines.\n- Reply in this order:\n  1. Overall portfolio summary\n  2. Best and weakest performer\n  3. Missing price note if applicable\n  4. Direct answer to the question\n\nClient profile:\nName: ${data.client_name || 'Client'}\nRisk profile: ${data.risk_profile || 'N/A'}\nGoal: ${data.goal || 'N/A'}\nTone: ${data.tone || 'simple'}\n\nClient question:\n${data.question}\n\nPortfolio summary:\n- Total invested across all holdings: \u20b9${Number(data.portfolio_summary.total_invested_all).toFixed(2)}\n- Total invested in priced holdings: \u20b9${Number(data.portfolio_summary.total_invested_priced).toFixed(2)}\n- Current value of priced holdings: \u20b9${Number(data.portfolio_summary.total_current_value).toFixed(2)}\n- Overall P&L on priced holdings: \u20b9${Number(data.portfolio_summary.total_pnl).toFixed(2)}\n- Return on priced holdings: ${Number(data.portfolio_summary.total_return_pct).toFixed(2)}%\n- Best performer: ${data.best_performer?.asset_name || 'N/A'} (${data.best_performer?.symbol || 'N/A'}), P&L ${data.best_performer?.pnl != null ? `\u20b9${Number(data.best_performer.pnl).toFixed(2)}` : 'N/A'}, return ${data.best_performer?.pnl_pct != null ? `${Number(data.best_performer.pnl_pct).toFixed(2)}%` : 'N/A'}\n- Weakest performer: ${data.weakest_performer?.asset_name || 'N/A'} (${data.weakest_performer?.symbol || 'N/A'}), P&L ${data.weakest_performer?.pnl != null ? `\u20b9${Number(data.weakest_performer.pnl).toFixed(2)}` : 'N/A'}, return ${data.weakest_performer?.pnl_pct != null ? `${Number(data.weakest_performer.pnl_pct).toFixed(2)}%` : 'N/A'}\n- Priced holdings count: ${data.portfolio_summary.priced_holdings_count}\n- Missing price count: ${data.portfolio_summary.missing_price_count}\n\nOptional market context:\n- Nifty change: ${data.market_context?.nifty_change_pct ?? 'N/A'}\n- Sensex change: ${data.market_context?.sensex_change_pct ?? 'N/A'}\n- Market tone: ${data.market_context?.market_tone ?? 'N/A'}\n- Market summary: ${data.market_context?.summary ?? 'N/A'}\n\nHoldings details:\n${holdingsText}\n\nMissing price holdings:\n${missingPricesText}\n          `.trim();\n\n          return [{\n            json: {\n              client_id: data.client_id,\n              client_name: data.client_name,\n              question: data.question,\n              source_channel: data.source_channel,\n              received_at: data.received_at,\n              prompt\n            }\n          }];"
      },
      "typeVersion": 2
    },
    {
      "id": "052d0285-c7c7-492a-b3da-ebe5a3474651",
      "name": "Generate AI Answer",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "onError": "continueRegularOutput",
      "position": [
        8848,
        2304
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3.1-flash-lite-preview",
          "cachedResultName": "models/gemini-3.1-flash-lite-preview"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "={{ $json.prompt }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "a4cc0070-579e-4953-b215-186445e120cb",
      "name": "Extract AI Answer",
      "type": "n8n-nodes-base.code",
      "position": [
        9152,
        2304
      ],
      "parameters": {
        "jsCode": "const parsed = $('Build AI Prompt').first().json;\nconst res = $input.first().json || {};\n\nconst reply =\n  res.content?.parts?.[0]?.text ||\n  res.candidates?.[0]?.content?.parts?.[0]?.text ||\n  res.text ||\n  null;\n\nif (!reply) {\n  return [{\n    json: {\n      success: false,\n      client_id: parsed.client_id,\n      question: parsed.question,\n      source_channel: parsed.source_channel,\n      received_at: parsed.received_at,\n      reply: null\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    success: true,\n    client_id: parsed.client_id,\n    question: parsed.question,\n    source_channel: parsed.source_channel,\n    received_at: parsed.received_at,\n    reply: String(reply).trim()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "0d66b8f7-e422-434e-adb5-032c9a9c10d1",
      "name": "Format Final Output",
      "type": "n8n-nodes-base.set",
      "position": [
        9328,
        2304
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={\n  \"success\": {{ $json.success }},\n  \"client_id\": \"{{$json.client_id}}\",\n  \"question\": \"{{$json.question}}\",\n  \"reply\": \"{{$json.reply}}\",\n  \"source_channel\": \"{{$json.source_channel}}\",\n  \"received_at\": \"{{$json.received_at}}\",\n  \"response_type\": \"portfolio_answer\",\n  \"timestamp\": $now.toISO()\n}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "4f21440d-a03d-408d-9e70-4274ee645c8b",
      "name": "IF AI Reply Exists?",
      "type": "n8n-nodes-base.if",
      "position": [
        9488,
        2304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ai_success_check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.success }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "67a023b7-9cad-4148-bc78-604d2f6ed1a5",
      "name": "Build AI Failed Response",
      "type": "n8n-nodes-base.code",
      "position": [
        9712,
        2400
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nreturn [{\n  json: {\n    status: 'failed',\n    error_stage: 'ai_generation',\n    action_taken: 'Stopped because AI reply was empty',\n    client_id: item.client_id || null,\n    question: item.question || null,\n    reply: 'AI could not generate a response right now. Please try again.',\n    source_channel: item.source_channel || 'chat',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4966f532-b5ea-47b7-be03-4a255875632a",
      "name": "Build Success Result",
      "type": "n8n-nodes-base.code",
      "position": [
        9712,
        2208
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nreturn [{\n  json: {\n    status: 'success',\n    error_stage: null,\n    action_taken: 'Generated portfolio reply',\n    client_id: item.client_id,\n    question: item.question,\n    reply: item.reply,\n    source_channel: item.source_channel || 'chat',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "783bc341-c57a-4924-81eb-da1bfa42a788",
      "name": "Log Result",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        9936,
        2592
      ],
      "parameters": {
        "columns": {
          "value": {
            "reply": "={{ $json.reply }}",
            "status": "={{ $json.status }}",
            "question": "={{ $json.question }}",
            "client_id": "={{ $json.client_id }}",
            "timestamp": "={{ $json.timestamp }}",
            "error_stage": "={{ $json.error_stage }}",
            "action_taken": "={{ $json.action_taken }}",
            "source_channel": "={{ $json.source_channel }}"
          },
          "schema": [
            {
              "id": "timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "client_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "client_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "question",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "question",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reply",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reply",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "action_taken",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "action_taken",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "error_stage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "error_stage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "source_channel",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "source_channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1803741055,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM/edit#gid=1803741055",
          "cachedResultName": "interaction_logs"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Utb59F9XfxP6j--TdE1HOApUk8Gv8CTIS2_BK3voTHM/edit?usp=drivesdk",
          "cachedResultName": "Portfolio_AI_System"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "7a37330e-719d-4611-95b8-687edb9fe4fa",
      "name": "Create Gmail Draft",
      "type": "n8n-nodes-base.gmail",
      "position": [
        9936,
        2784
      ],
      "parameters": {
        "message": "={{ $json.reply }}",
        "options": {},
        "subject": "={{ 'Portfolio reply for ' + ($json.client_id || 'client') }}",
        "resource": "draft"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "7e1c0e4b-5ba7-4e88-8479-da78cf639741",
      "name": "Return Final Output",
      "type": "n8n-nodes-base.code",
      "position": [
        10160,
        2688
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    final_status: $json.status,\n    client_id: $json.client_id,\n    question: $json.question,\n    reply: $json.reply,\n    action_taken: $json.action_taken\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7206a880-de32-401c-81c9-81d0849118d6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5520,
        1888
      ],
      "parameters": {
        "width": 1184,
        "height": 352,
        "content": "## Client Portfolio Assistant\n\n### How it works\nThis workflow takes a client message (e.g., \"C001: question\"), validates the format and identifies the client. It fetches client profile and holdings from Google Sheets, pulls live market prices and calculates portfolio metrics like total value, P&L and returns. Optional market context is added for better insights. An AI model then generates a short, accurate and client-friendly response based only on this data. All interactions are logged for tracking and reliability.\n\n### Setup steps\n1. Create Google Sheets with clients, holdings and logs.\n2. Connect Google Sheets credentials in n8n.\n3. Add live price API endpoint.\n4. Add market summary API (optional).\n5. Configure AI node (Gemini or HF).\n6. Test using: `C001: your question`"
      },
      "typeVersion": 1
    },
    {
      "id": "d45905e9-459c-42d1-bb42-e606485af00f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5536,
        2432
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 784,
        "content": "##  Input Handling & Validation\nThis section handles incoming chat messages and ensures they follow the correct format.\n\nIt extracts the client ID and question and checks for common issues like empty input or missing separators. If validation fails, the workflow immediately stops, logs the issue and returns a clean error response.\n\nThis keeps the rest of the workflow safe from bad data."
      },
      "typeVersion": 1
    },
    {
      "id": "d2a9527e-e203-4c1c-8a0c-22d8a36e2097",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6192,
        2336
      ],
      "parameters": {
        "color": 7,
        "width": 832,
        "height": 448,
        "content": "## Client & Portfolio Lookup\nHere, the workflow fetches client profile and holdings from Google Sheets using the client ID.\n\nIt also prepares stock symbols required for fetching live prices. If the client doesn\u2019t exist or no holdings are found, the workflow stops early and returns a clear message.\n\nThis ensures only valid and complete data moves forward."
      },
      "typeVersion": 1
    },
    {
      "id": "1a06b4de-8bd0-418d-a471-1eb1e0cd14d6",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7264,
        2144
      ],
      "parameters": {
        "color": 7,
        "width": 896,
        "height": 432,
        "content": "##  Price Fetch & Portfolio Calculation\nThis part calls the live market API to fetch prices for all holdings.\n\nIt then merges this data with the portfolio and calculates key metrics like invested value, current value, profit/loss and returns. It also identifies the best and weakest performers and tracks any missing price data.\n\nThis step builds a complete and accurate portfolio snapshot."
      },
      "typeVersion": 1
    },
    {
      "id": "b902a2fd-015f-4935-888b-e47ef4032091",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8208,
        2096
      ],
      "parameters": {
        "color": 7,
        "width": 1072,
        "height": 432,
        "content": "## AI Answer Generation\nA structured prompt is created using portfolio data, client profile and optional market context.\n\nThe AI generates a short, clear response based strictly on the provided data. Rules are applied to avoid incorrect calculations or assumptions.\n\nThe final answer is extracted and cleaned before sending forward."
      },
      "typeVersion": 1
    },
    {
      "id": "4110e681-918a-4655-8fab-4714acf574d5",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        9312,
        2000
      ],
      "parameters": {
        "color": 7,
        "width": 896,
        "height": 1040,
        "content": "##  Final Response & Logging\nThe workflow formats the final output with the reply, client details and status.\n\nIt can also log interactions or be extended to send responses to other systems like CRM, email or Slack.\n\nThis acts as the final delivery layer of the workflow."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "4ea4fc78-6026-44ef-a217-03b3f05c2c70",
  "connections": {
    "Log Result": {
      "main": [
        [
          {
            "node": "Return Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Prompt": {
      "main": [
        [
          {
            "node": "Generate AI Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Live Prices": {
      "main": [
        [
          {
            "node": "Normalize Price Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Valid Input?": {
      "main": [
        [
          {
            "node": "Get Client Profile",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Invalid Input Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Symbols": {
      "main": [
        [
          {
            "node": "IF Holdings Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Client Found?": {
      "main": [
        [
          {
            "node": "Get Client Holdings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Client Not Found Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract AI Answer": {
      "main": [
        [
          {
            "node": "Format Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Invalid Input": {
      "main": [
        [
          {
            "node": "Return Invalid Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Gmail Draft": {
      "main": [
        [
          {
            "node": "Return Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Answer": {
      "main": [
        [
          {
            "node": "Extract AI Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Client Profile": {
      "main": [
        [
          {
            "node": "IF Client Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Market Context": {
      "main": [
        [
          {
            "node": "Attach Market Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Holdings Found?": {
      "main": [
        [
          {
            "node": "Get Live Prices",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build No Holdings Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Final Output": {
      "main": [
        [
          {
            "node": "IF AI Reply Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Client Holdings": {
      "main": [
        [
          {
            "node": "Prepare Symbols",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF AI Reply Exists?": {
      "main": [
        [
          {
            "node": "Build Success Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build AI Failed Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Success Result": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Price API Worked?": {
      "main": [
        [
          {
            "node": "Merge Portfolio Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build API Failed Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Portfolio Data": {
      "main": [
        [
          {
            "node": "Get Market Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Client Message": {
      "main": [
        [
          {
            "node": "IF Valid Input?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Market Context": {
      "main": [
        [
          {
            "node": "Build AI Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Failed Response": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Price Response": {
      "main": [
        [
          {
            "node": "IF Price API Worked?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build API Failed Response": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build No Holdings Response": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Parse Client Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Invalid Input Response": {
      "main": [
        [
          {
            "node": "Log Invalid Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Client Not Found Response": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}