{
  "id": "3y5cbX89D0mFj9N5",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Recommender: From Food Photo to Restaurant and Book (Google Books Integrated) ## What it does  Analyzes a food photo with",
  "tags": [],
  "nodes": [
    {
      "id": "1269407f-2761-4611-af3c-d61e9fbeb614",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        1680,
        800
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": "1uoky4OAFYULqTrWkkfm8JFGBGNcLaLT0",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1uoky4OAFYULqTrWkkfm8JFGBGNcLaLT0",
          "cachedResultName": "n8n_folder"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d804b5e1-f574-4c81-8c57-f9de592df449",
      "name": "Dish Classifier",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2128,
        800
      ],
      "parameters": {
        "text": "={\n  \"model\": \"gpt-4o-mini\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are a strict JSON generator. Always return a JSON object that includes only the following keys: dish_name, category, calories_kcal, protein_g, fat_g, carbs_g. Do not include any explanations.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"image_url\",\n          \"image_url\": {\n            \"url\": \"https://drive.google.com/uc?export=download&id={{$json.id}}\"\n          }\n        },\n        {\n          \"type\": \"text\",\n          \"text\": \"From the image, return the dish name, category, and estimated nutrition per serving. Output only valid JSON.\"\n        }\n      ]\n    }\n  ],\n  \"response_format\": { \"type\": \"json_object\" }\n}\n",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "d1f36934-6055-4c93-9396-fb7ef3377ddb",
      "name": "Fetch Image (Drive)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1904,
        800
      ],
      "parameters": {
        "url": "https://drive.google.com/uc?id=1DRoHd4SVbu3lvw6pH5oWAKUf-TejnVXT&export=download",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "0ed6a504-a545-4bc2-a10b-902d8d21d299",
      "name": "LLM (Vision/JSON)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        2208,
        1024
      ],
      "parameters": {
        "model": "openai/gpt-5",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6862ca06-be98-4a63-899b-b94f02969447",
      "name": "Normalize Classification",
      "type": "n8n-nodes-base.code",
      "position": [
        2480,
        800
      ],
      "parameters": {
        "jsCode": "// --- Dish Classifier \u306e output(JSON\u6587\u5b57\u5217) \u3092\u30d1\u30fc\u30b9 ---\nlet parsed = {};\ntry {\n  parsed = JSON.parse($json.output || '{}');\n} catch (e) {\n  parsed = {};\n}\n\n// --- \u30ab\u30c6\u30b4\u30ea\u53d6\u5f97 ---\nconst raw = parsed.category || '';\nconst top = raw.split('/')[0].trim(); // \"\u4e2d\u83ef\" \u306e\u3088\u3046\u306b\u5148\u982d\u90e8\u5206\u3092\u62bd\u51fa\n\n// --- \u30de\u30c3\u30d4\u30f3\u30b0\u5b9a\u7fa9 ---\nconst mapping = {\n  '\u548c\u98df':    { type: 'restaurant', keyword: '\u548c\u98df \u5b9a\u98df \u305d\u3070 \u3046\u3069\u3093' },\n  '\u6d0b\u98df':    { type: 'restaurant', keyword: '\u6d0b\u98df \u30d3\u30b9\u30c8\u30ed \u30d1\u30b9\u30bf \u30cf\u30f3\u30d0\u30fc\u30b0' },\n  '\u4e2d\u83ef':    { type: 'restaurant', keyword: '\u4e2d\u83ef \u30e9\u30fc\u30e1\u30f3 \u9903\u5b50 \u30c1\u30e3\u30fc\u30cf\u30f3' },\n  '\u30e9\u30fc\u30e1\u30f3': { type: 'restaurant', keyword: '\u30e9\u30fc\u30e1\u30f3 \u3064\u3051\u9eba \u4e2d\u83ef\u305d\u3070' },\n  '\u5bff\u53f8':    { type: 'restaurant', keyword: '\u5bff\u53f8 \u3059\u3057 \u9b5a\u4ecb' },\n  '\u30ab\u30ec\u30fc':  { type: 'restaurant', keyword: '\u30ab\u30ec\u30fc \u30b9\u30d1\u30a4\u30b9 \u30a4\u30f3\u30c9\u6599\u7406' },\n  '\u30c7\u30b6\u30fc\u30c8': { type: 'cafe', keyword: '\u30b9\u30a4\u30fc\u30c4 \u30b1\u30fc\u30ad \u30d1\u30d5\u30a7 \u30c7\u30b6\u30fc\u30c8' }\n};\n\n// --- \u30de\u30c3\u30d4\u30f3\u30b0\u9069\u7528 ---\nconst conf = mapping[top] || { type: 'restaurant', keyword: top || '\u30ec\u30b9\u30c8\u30e9\u30f3' };\n\n// --- \u51fa\u529b ---\nreturn {\n  json: {\n    dish_name: parsed.dish_name || '\u4e0d\u660e',\n    category: top || '\u672a\u5206\u985e',\n    confidence: parsed.confidence || 0,\n    ...conf\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "8e43608e-0602-4cea-bab8-72e66251df8f",
      "name": "Set Origin & Radius",
      "type": "n8n-nodes-base.set",
      "position": [
        2704,
        800
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "95af2f82-9212-4d14-87ac-ae42ef08f3ec",
              "name": "originLabel",
              "type": "string",
              "value": "\u4e2d\u91ce\u99c5 \u6771\u4eac\u90fd\u4e2d\u91ce\u533a"
            },
            {
              "id": "f2d60ee4-287e-4d7a-9e52-87248e2c550d",
              "name": "originLat",
              "type": "number",
              "value": "=35.7073"
            },
            {
              "id": "ddfd5750-6524-48d8-aead-11903a434972",
              "name": "radiusM",
              "type": "number",
              "value": "=1200"
            },
            {
              "id": "8e8d2e00-7069-4558-bc42-d3e64b307cc1",
              "name": "originLng",
              "type": "string",
              "value": "139.6655"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "985089c9-6a40-41b4-a344-6442de7c2d20",
      "name": "Search Google Places",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3152,
        800
      ],
      "parameters": {
        "url": "https://places.googleapis.com/v1/places:searchText",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({\n  textQuery: $('Normalize Classification').item.json.category || \"\u30ec\u30b9\u30c8\u30e9\u30f3\",\n  languageCode: \"ja\",\n  maxResultCount: 3,\n  locationBias: {\n    circle: {\n      center: {\n        latitude: Number($('Set Origin & Radius').item.json.originLat),\n        longitude: Number($('Set Origin & Radius').item.json.originLng)\n      },\n      radius: Number($('Set Origin & Radius').item.json.radiusM ?? 1000)\n    }\n  }\n}) }}\n",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-Goog-Api-Key",
              "value": "={{ $json[\"GOOGLE_API_KEY\"] }}"
            },
            {
              "name": "X-Goog-FieldMask",
              "value": "places.displayName,places.formattedAddress,places.rating,places.id"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "e42be32f-f334-4570-9de1-12efe2b94024",
      "name": "Summarize Place List",
      "type": "n8n-nodes-base.code",
      "position": [
        3376,
        800
      ],
      "parameters": {
        "jsCode": "// Google Places \u306e\u51fa\u529b\u304b\u3089\u5fc5\u8981\u306a\u60c5\u5831\u3060\u3051\u62bd\u51fa\nconst places = $json.places.map(p => ({\n  name: p.displayName?.text || \"\u4e0d\u660e\",\n  rating: p.rating || 0,\n  reviews: p.userRatingCount || 0,\n  address: p.shortFormattedAddress || \"\u4f4f\u6240\u4e0d\u660e\"\n}));\n\n// AI\u306b\u6e21\u3059\u305f\u3081\u306e\u7c21\u6f54\u306a\u30ea\u30b9\u30c8\u6587\u5b57\u5217\u3092\u751f\u6210\nconst summary = places.map(\n  p => `${p.name}\uff08\u8a55\u4fa1: ${p.rating}, \u30ec\u30d3\u30e5\u30fc\u6570: ${p.reviews}, \u4f4f\u6240: ${p.address}\uff09`\n).join(\"\\n\");\n\nreturn {\n  json: {\n    places,\n    summary\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "98a9cba5-0d79-4641-ac49-a4e2cdfd5a8f",
      "name": "LLM (Selection)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        3680,
        1024
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7c8508b1-cede-451b-9844-04e7106e2d8c",
      "name": "Select Best Place (AI)",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3600,
        800
      ],
      "parameters": {
        "text": "=You are a strict evaluator. Use ONLY the data provided below.\n\nHere is the restaurant list:\n{{ $json.summary }}\n\nFrom the above list, select the single best restaurant based on:\n1. Highest rating\n2. If ratings are equal, most user reviews\n\nReturn the result ONLY in this JSON format:\n\n{\n  \"best_place\": \"\u5e97\u8217\u540d\",\n  \"rating\": \u6570\u5024,\n  \"reviews\": \u6570\u5024,\n  \"address\": \"\u4f4f\u6240\",\n  \"reason\": \"\u9078\u3093\u3060\u7406\u7531\uff0830\u6587\u5b57\u4ee5\u5185\uff09\"\n}\n",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "c5c9a61e-d894-40ff-aba1-13d86f9af726",
      "name": "Format Best Place JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        3952,
        800
      ],
      "parameters": {
        "jsCode": "// AI Agent\u306e\u51fa\u529b\uff08\u6587\u5b57\u5217\uff09\u3092\u53d6\u5f97\nconst raw = $json.output || \"\";\n\n// JSON\u3092\u5b89\u5168\u306b\u30d1\u30fc\u30b9\nlet parsed = {};\ntry {\n  parsed = JSON.parse(raw);\n} catch (e) {\n  parsed = {};\n}\n\n// \u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u660e\u793a\u7684\u306b\u6574\u5f62\nreturn {\n  best_place: parsed.best_place || \"\",\n  reason: parsed.reason || \"\",\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f721cd9f-30eb-4fc7-8810-2d02291ace90",
      "name": "LLM (Books)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        4256,
        1024
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "99be8853-41de-44ed-8596-657a0a8fedac",
      "name": "Recommend Book (AI)",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        4176,
        800
      ],
      "parameters": {
        "text": "=You are a creative book curator.\nGiven the name of a restaurant, infer its theme, cuisine, or atmosphere,\nand recommend one *real* book that exists on Google Books which matches the vibe.\n\nReturn ONLY JSON (no markdown, no prose). Fill all fields (no empty strings).\n\nInput:\n{{ $json.best_place }}\n\nOutput JSON fields:\n- restaurant: echo the input restaurant name (string)\n- recommended_book.title: book title (string, non-empty, real book)\n- recommended_book.author: author name (string, non-empty)\n- reason: why this book fits the restaurant (<=100 Japanese chars)\n",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "b22512e2-07f1-4b25-89b0-2026305f2341",
      "name": "Normalize Book JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        4528,
        800
      ],
      "parameters": {
        "jsCode": "// 1) AI\u51fa\u529b\u3092\u62fe\u3046\uff08\u3059\u3067\u306b\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306a\u3089\u305d\u308c\u3092\u4f7f\u3046\uff09\nlet raw = $json.output ?? $json.data ?? $json.text ?? $json;\nlet parsed = {};\n\n// 2) \u6587\u5b57\u5217\u306a\u3089\u524d\u5f8c\u306e\u4f59\u5206\u3092\u9664\u53bb\uff06```json\u30d5\u30a7\u30f3\u30b9\u3092\u5265\u304c\u3059\nif (typeof raw === 'string') {\n  let s = raw.trim();\n  s = s.replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();\n  try {\n    parsed = JSON.parse(s);\n  } catch {\n    parsed = {};\n  }\n} else if (typeof raw === 'object') {\n  parsed = raw;\n}\n\n// 3) \u6700\u4f4e\u9650\u306e\u30ad\u30fc\u3092\u7528\u610f\uff08\u7a7a\u3067\u3082\u69cb\u9020\u306f\u4f5c\u308b\uff09\nparsed.recommended_book = parsed.recommended_book || {};\nparsed.recommended_book.title = parsed.recommended_book.title || '';\nparsed.recommended_book.author = parsed.recommended_book.author || '';\nparsed.reason = parsed.reason || '';\nparsed.restaurant = parsed.restaurant || '';\n\nreturn parsed;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9af6fc3d-aabd-4f02-aa94-a96491125968",
      "name": "Search Google Books",
      "type": "n8n-nodes-base.googleBooks",
      "position": [
        4752,
        800
      ],
      "parameters": {
        "resource": "volume",
        "operation": "getAll",
        "searchQuery": "={{ '\"' + $json.recommended_book.title + '\"' }}"
      },
      "credentials": {
        "googleBooksOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "41f90290-f831-43b5-ac26-d1159611763f",
      "name": "Format Book Details",
      "type": "n8n-nodes-base.code",
      "position": [
        4976,
        800
      ],
      "parameters": {
        "jsCode": "// \u300cGet many volumes\u300d\u30ce\u30fc\u30c9\u306e 1 \u51fa\u529b\u76ee(run 0 / output 0)\u306e\u914d\u5217\u3092\u53d6\u5f97\nconst vols = $items(\"Search Google Books\", 0, 0);\n\n// \u30a2\u30a4\u30c6\u30e0\u304c\u306a\u3051\u308c\u3070\u7a7a\u3067\u8fd4\u3059\nif (!vols || vols.length === 0) {\n  return [{ json: { title: \"\uff08\u30bf\u30a4\u30c8\u30eb\u4e0d\u660e\uff09\" } }];\n}\n\n// \u5148\u982d\u306e\u672c\u3060\u3051\u4f7f\u3046\uff08\u5fc5\u8981\u306a\u3089\u4efb\u610f\u306e\u9078\u3073\u65b9\u306b\u5909\u66f4\uff09\nconst v = vols[0].json || {};\nconst vi = v.volumeInfo || {};\n\nreturn [\n  {\n    json: {\n      title: vi.title ?? \"\uff08\u30bf\u30a4\u30c8\u30eb\u4e0d\u660e\uff09\",\n      authors: Array.isArray(vi.authors) ? vi.authors.join(\", \") : \"\uff08\u8457\u8005\u60c5\u5831\u306a\u3057\uff09\",\n      publisher: vi.publisher ?? \"\uff08\u51fa\u7248\u793e\u4e0d\u660e\uff09\",\n      publishedDate: vi.publishedDate ?? \"\u2015\",\n      selfLink: v.selfLink ?? \"\"\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "26c9dce5-bda6-4a5e-b474-fe1148ac73f1",
      "name": "Post to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        5200,
        800
      ],
      "parameters": {
        "text": "=The dish in the photo is {{$node[\"Normalize Classification\"].json[\"dish_name\"]}}, and its category is {{$node[\"Normalize Classification\"].json[\"category\"]}}.\nThe best {{$node[\"Normalize Classification\"].json[\"category\"]}} spot around Nakano Station, Nakano City, Tokyo is {{$node[\"Format Best Place JSON\"].json[\"best_place\"]}}.\n\nA book that pairs well with this place\u2026\n\n\ud83d\udcd6 Title: {{$json.title}}\n\ud83d\udc64 Author(s): {{$json.authors}}\n\ud83c\udfe2 Publisher: {{$json.publisher}}\n\ud83d\udcc5 Published: {{$json.publishedDate}}\n\ud83d\udd17 Link: <{{$json.selfLink}}|Google Books>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "CKUCBTG0H",
          "cachedResultName": "general"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4b6256c4-9c26-4d24-abee-0d7800b121c6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        592
      ],
      "parameters": {
        "width": 560,
        "height": 960,
        "content": "## AI Recommender: From Food Photo to Restaurant and Book (Google Books Integrated)\n## What it does\n\nAnalyzes a food photo with an AI vision model to extract dish name + category\n\nSearches nearby restaurants with Google Places and selects the single best (rating \u2192 reviews tie-break)\n\nFinds a matching book via Google Books and posts a tidy summary to Slack\n\n## Who it\u2019s for\n\nFoodies, bloggers, and teams who want a plug-and-play flow that turns a single food photo into a dining pick + themed reading.\n\n## How it works\n\nGoogle Drive Trigger detects a new photo\n\nDish Classifier (Vision LLM) \u2192 JSON (dish_name, category, basic macros)\n\nSearch Google Places near your origin; Select Best Place (AI)\n\nRecommend Book (AI) \u2192 Search Google Books \u2192 format details\n\nPost to Slack (JP/EN both possible)\n\n## Requirements\n\nGoogle Drive / Google Places / Google Books credentials, LLM access (OpenRouter/OpenAI), Slack OAuth.\n\n## Customize\n\nEdit origin/radius in Set Origin & Radius, tweak category\u2192keyword mapping in Normalize Classification, adjust Slack channel & message in Post to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "0f73fbe4-f430-446e-b5a0-3ed56c6a04a3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 512,
        "content": "## \ud83e\udde9 Setup & Credentials\n\nConnect these **before running** the workflow:\n\n- **Google Drive Trigger** \u2192 Drive OAuth  \n- **Search Google Places** \u2192 Move API key to a Credential (don\u2019t hardcode in headers)  \n- **Search Google Books** \u2192 Books OAuth  \n- **LLM nodes (Vision / Selection / Books)** \u2192 OpenRouter / OpenAI  \n- **Post to Slack** \u2192 Slack OAuth2  \n\n### Config (Set node)\nKeep all user-editable variables centralized here:\n\n- `originLabel` (e.g., \u201cNakano Station, Tokyo\u201d)  \n- `originLat`, `originLng` (coordinates)  \n- `radiusM` (search radius, meters)\n\n\ud83d\udca1 *Add your own Google API key here or via Credential for security and reusability.*\n"
      },
      "typeVersion": 1
    },
    {
      "id": "f9164e78-b671-475b-9f3c-2538b9b230a4",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2064,
        1168
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 256,
        "content": "## Photo \u2192 Category\n\nFetch Image (Drive) \u2013 downloads the image\n\nDish Classifier \u2013 vision prompt; returns strict JSON\n\nNormalize Classification (Code) \u2013 parses JSON safely, maps category to Places-friendly keyword/type; outputs dish_name, category, type, keyword"
      },
      "typeVersion": 1
    },
    {
      "id": "9e12b78c-2ed0-40d7-9ed2-8d25dda64d89",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2976,
        1056
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 352,
        "content": "## Find & Select Restaurant\n\nSet Origin & Radius (Set) \u2013 coordinates/radius\n\nSearch Google Places (HTTP POST /v1/places:searchText) \u2013 gets top candidates\n\nSummarize Place List (Code) \u2013 flattens to readable summary\n\nSelect Best Place (AI) \u2013 strictly picks 1 by rating, then user reviews\n\nFormat Best Place JSON (Code) \u2013 outputs { best_place, reason }"
      },
      "typeVersion": 1
    },
    {
      "id": "dd635986-5749-40fc-b7a2-2f5217404339",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3904,
        1184
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 304,
        "content": "## Book Recommendation\n\nRecommend Book (AI) \u2013 returns recommended_book.title/author + reason\n\nNormalize Book JSON (Code) \u2013 strips code fences, guarantees fields\n\nSearch Google Books \u2013 quoted title search for precision\n\nFormat Book Details (Code) \u2013 single record: title, authors, publisher, publishedDate, selfLink"
      },
      "typeVersion": 1
    },
    {
      "id": "8b4f9ed3-0ba1-4d0d-ac50-665041345636",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4816,
        1040
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 128,
        "content": "## Output\n\nPost to Slack \u2013 assembles dish/category + best place + book details"
      },
      "typeVersion": 1
    },
    {
      "id": "3653a047-b561-46f0-a730-c1ebb026274a",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5344,
        1056
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 416,
        "content": "## Troubleshooting\n\nLLM returns prose, not JSON \u2192 tighten prompts; enable \u201cspecific output format\u201d; add a Code node to parse & default missing fields\n\nWrong book results \u2192 ensure quoted title / intitle: / langRestrict\n\nSlack shows undefined \u2192 execute previous nodes; verify field paths; prefer a Code step that outputs a single, flat record for Slack\n\nPlaces returns off-topic venues \u2192 adjust mapping keywords; increase maxResultCount; tweak radius or origin\n\nRate limits \u2192 slow Drive polling; add batching/schedules"
      },
      "typeVersion": 1
    },
    {
      "id": "5e8d70ee-a1fc-4dc7-a0e6-be208c3c1b1d",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5344,
        608
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 416,
        "content": "## Security & Review Checklist\n\n\u2705 No hardcoded secrets in HTTP headers/body\u2014store API keys in Credentials\n\n\u2705 Remove personal IDs (private Sheet IDs, channels) before publishing\n\n\u2705 Centralize user settings in a Set (Config) node\n\n\u2705 Node names are descriptive and consistent\n\n\u2705 Add a small workflow image or short Loom for setup (recommended)"
      },
      "typeVersion": 1
    },
    {
      "id": "3f180369-5d9f-4802-bae0-4ac007ed6e0a",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4880,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 416,
        "content": "## Google Books & Slack Output (quality tips)\n\nImprove Books accuracy\n\nUse quoted title: searchQuery = \"={{ '\\\"' + $json.recommended_book.title + '\\\"' }}\"\n\nIf needed, add intitle:\"{{title}}\" or restrict language via langRestrict (ja/en)\n\nSlack formatting\n\nKeep a single message with clear sections, or send JP then reply with EN using thread TS\n\nUse <URL|Label> to avoid noisy link previews; disable \u201cUnfurl Links/Media\u201d if needed"
      },
      "typeVersion": 1
    },
    {
      "id": "8d2c4e24-cfd8-4b0c-bf31-2e672196923b",
      "name": "Pass the API",
      "type": "n8n-nodes-base.set",
      "position": [
        2928,
        800
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "{\n  \"GOOGLE_API_KEY\": \"\"\n}"
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f64c9658-7481-4e4e-8ed2-083fffa4bc2d",
  "connections": {
    "LLM (Books)": {
      "ai_languageModel": [
        [
          {
            "node": "Recommend Book (AI)",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Pass the API": {
      "main": [
        [
          {
            "node": "Search Google Places",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dish Classifier": {
      "main": [
        [
          {
            "node": "Normalize Classification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM (Selection)": {
      "ai_languageModel": [
        [
          {
            "node": "Select Best Place (AI)",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "LLM (Vision/JSON)": {
      "ai_languageModel": [
        [
          {
            "node": "Dish Classifier",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Image (Drive)": {
      "main": [
        [
          {
            "node": "Dish Classifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Book Details": {
      "main": [
        [
          {
            "node": "Post to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Book JSON": {
      "main": [
        [
          {
            "node": "Search Google Books",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recommend Book (AI)": {
      "main": [
        [
          {
            "node": "Normalize Book JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google Books": {
      "main": [
        [
          {
            "node": "Format Book Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Origin & Radius": {
      "main": [
        [
          {
            "node": "Pass the API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Fetch Image (Drive)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google Places": {
      "main": [
        [
          {
            "node": "Summarize Place List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Place List": {
      "main": [
        [
          {
            "node": "Select Best Place (AI)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Best Place JSON": {
      "main": [
        [
          {
            "node": "Recommend Book (AI)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Best Place (AI)": {
      "main": [
        [
          {
            "node": "Format Best Place JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Classification": {
      "main": [
        [
          {
            "node": "Set Origin & Radius",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}