From 307c443f56ce2ba2898f7be32841b855b4e0e49a Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Wed, 14 Jan 2026 22:28:16 -0800 Subject: [PATCH] Add addresses debug endpoint --- api/Application.cfm | 10 +- api/setup/analyzeMenuImages.cfm | 203 +++++++++++++++++++------------- api/setup/testUpload.cfm | 85 +++++++++++++ 3 files changed, 216 insertions(+), 82 deletions(-) create mode 100644 api/setup/testUpload.cfm diff --git a/api/Application.cfm b/api/Application.cfm index 5285446..ceff646 100644 --- a/api/Application.cfm +++ b/api/Application.cfm @@ -1,4 +1,4 @@ - + - + + + + + + @@ -91,6 +96,7 @@ if (len(request._api_path)) { if (findNoCase("/api/beacons/getBusinessFromBeacon.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/menu/items.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/addresses/states.cfm", request._api_path)) request._api_isPublic = true; + if (findNoCase("/api/addresses/debug.cfm", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/debug/", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/admin/", request._api_path)) request._api_isPublic = true; if (findNoCase("/api/orders/getOrCreateCart.cfm", request._api_path)) request._api_isPublic = true; diff --git a/api/setup/analyzeMenuImages.cfm b/api/setup/analyzeMenuImages.cfm index ec7e35f..602a625 100644 --- a/api/setup/analyzeMenuImages.cfm +++ b/api/setup/analyzeMenuImages.cfm @@ -24,11 +24,20 @@ CLAUDE_API_KEY = application.claudeApiKey ?: ""; // If not in application scope, try to read from config if (!len(CLAUDE_API_KEY)) { try { - configPath = expandPath("/biz.payfrit.com/config/claude.json"); + // Try relative path from current file location first + configPath = getDirectoryFromPath(getCurrentTemplatePath()) & "../../config/claude.json"; if (fileExists(configPath)) { configData = deserializeJSON(fileRead(configPath)); CLAUDE_API_KEY = configData.apiKey ?: ""; } + // Fallback to expandPath if relative path didn't work + if (!len(CLAUDE_API_KEY)) { + configPath = expandPath("/biz.payfrit.com/config/claude.json"); + if (fileExists(configPath)) { + configData = deserializeJSON(fileRead(configPath)); + CLAUDE_API_KEY = configData.apiKey ?: ""; + } + } } catch (any e) { // Config file doesn't exist or is invalid } @@ -47,7 +56,7 @@ try { // Get form fields for file uploads (file0, file1, file2, etc.) formFields = form.keyArray(); for (fieldName in formFields) { - if (reFindNoCase("^file\d+$", fieldName) && len(form[fieldName])) { + if (reFindNoCase("^file[0-9]+$", fieldName) && len(form[fieldName])) { // This is a file field arrayAppend(uploadedFiles, fieldName); } @@ -67,18 +76,56 @@ try { } for (fieldName in uploadedFiles) { - // Upload the file - uploadResult = fileUpload( - uploadDir, - fieldName, - "image/jpeg,image/png,image/gif,image/webp,application/pdf", - "makeunique" - ); + try { + // Upload the file + uploadResult = fileUpload( + uploadDir, + fieldName, + "image/jpeg,image/png,image/gif,image/webp,application/pdf", + "makeunique" + ); + } catch (any uploadErr) { + throw(message="File upload failed: " & uploadErr.message, detail="Field: " & fieldName); + } if (uploadResult.fileWasSaved) { filePath = uploadResult.serverDirectory & "/" & uploadResult.serverFile; fileExt = lCase(uploadResult.serverFileExt); + // For images, resize if too large (max 1600px on longest side) + if (listFindNoCase("jpg,jpeg,png,gif,webp", fileExt)) { + try { + img = imageRead(filePath); + } catch (any readErr) { + throw(message="Image read failed: " & readErr.message, detail="File: " & filePath); + } + + imgWidth = img.width; + imgHeight = img.height; + maxDimension = 1600; + + if (imgWidth > maxDimension || imgHeight > maxDimension) { + try { + if (imgWidth > imgHeight) { + newWidth = maxDimension; + newHeight = int(imgHeight * (maxDimension / imgWidth)); + } else { + newHeight = maxDimension; + newWidth = int(imgWidth * (maxDimension / imgHeight)); + } + imageResize(img, newWidth, newHeight); + } catch (any resizeErr) { + throw(message="Image resize failed: " & resizeErr.message, detail="Dimensions: " & imgWidth & "x" & imgHeight); + } + } + // Re-save with good quality compression + try { + imageWrite(img, filePath, 0.8); + } catch (any writeErr) { + throw(message="Image write failed: " & writeErr.message, detail="File: " & filePath); + } + } + // Read file and convert to base64 fileContent = fileReadBinary(filePath); base64Content = toBase64(fileContent); @@ -90,14 +137,26 @@ try { else if (fileExt == "webp") mediaType = "image/webp"; else if (fileExt == "pdf") mediaType = "application/pdf"; - arrayAppend(imageDataArray, { - "type": "image", - "source": { - "type": "base64", - "media_type": mediaType, - "data": base64Content - } - }); + // Claude API uses different structure for PDFs vs images + if (fileExt == "pdf") { + arrayAppend(imageDataArray, { + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": base64Content + } + }); + } else { + arrayAppend(imageDataArray, { + "type": "image", + "source": { + "type": "base64", + "media_type": mediaType, + "data": base64Content + } + }); + } // Clean up temp file fileDelete(filePath); @@ -111,56 +170,7 @@ try { // Build the prompt for Claude systemPrompt = "You are an expert at extracting structured menu data from restaurant menu images. Your task is to analyze menu images and return the data in a specific JSON format. Be thorough and extract ALL items, categories, and any modifier patterns you can identify."; - userPrompt = "Please analyze these menu images and extract all the information into the following JSON structure: - -{ - ""business"": { - ""name"": ""Restaurant name if visible"", - ""address"": ""Full address if visible"", - ""phone"": ""Phone number(s) if visible"", - ""hours"": ""Business hours if visible"" - }, - ""categories"": [ - { - ""name"": ""Category name"", - ""itemCount"": 0 - } - ], - ""modifiers"": [ - { - ""name"": ""Modifier template name (e.g., 'Size', 'Protein Choice', 'Toast Type')"", - ""required"": true or false, - ""options"": [ - { ""name"": ""Option name"", ""price"": 0.00 } - ] - } - ], - ""items"": [ - { - ""name"": ""Item name"", - ""description"": ""Item description if any"", - ""price"": 0.00, - ""category"": ""Category this item belongs to"", - ""modifiers"": [""Name of applicable modifier template""] - } - ] -} - -Important guidelines: -1. Extract EVERY menu item you can see, with accurate prices -2. Group items into their categories as shown on the menu -3. Look for modifier patterns like: - - Size options (Small/Regular/Large) - - Protein choices (Chicken/Beef/Shrimp) - - Bread choices (Wheat/Sourdough/White/Rye) - - Add-ons with prices (Add bacon +$2.00) - - Required choices (served with choice of side) -4. For modifiers that appear on multiple items, create a reusable template -5. Assign the appropriate modifier templates to each item -6. If a price has a '+' it's usually an upcharge modifier option -7. Pay attention to category headers and section dividers - -Return ONLY the JSON object, no other text."; + userPrompt = "Please analyze these menu images and extract all the information into a JSON object with these keys: business (with name, address, phone, hours), categories (array with name and itemCount), modifiers (array with name, required boolean, and options array), and items (array with name, description, price, category, and modifiers array). Extract EVERY menu item with accurate prices. Group items by category. Look for modifier patterns like sizes, protein choices, bread choices, and add-ons. Create reusable modifier templates for patterns that appear on multiple items. Return ONLY valid JSON, no other text."; // Build the messages array with images messagesContent = []; @@ -176,14 +186,7 @@ Return ONLY the JSON object, no other text."; "text": userPrompt }); - // Call Claude API - httpService = new http(); - httpService.setMethod("POST"); - httpService.setUrl("https://api.anthropic.com/v1/messages"); - httpService.addParam(type="header", name="Content-Type", value="application/json"); - httpService.addParam(type="header", name="x-api-key", value=CLAUDE_API_KEY); - httpService.addParam(type="header", name="anthropic-version", value="2023-06-01"); - + // Build request body requestBody = { "model": "claude-sonnet-4-20250514", "max_tokens": 8192, @@ -196,16 +199,51 @@ Return ONLY the JSON object, no other text."; ] }; - httpService.addParam(type="body", value=serializeJSON(requestBody)); + // Write request body to temp file (Lucee's HTTP client has issues with large payloads) + tempRequestFile = getTempDirectory() & "claude_request_" & createUUID() & ".json"; + tempResponseFile = getTempDirectory() & "claude_response_" & createUUID() & ".json"; + fileWrite(tempRequestFile, serializeJSON(requestBody)); - httpResult = httpService.send().getPrefix(); + try { + // Call Claude API using cfhttp tag (more reliable than http component) + httpResult = {}; + http url="https://api.anthropic.com/v1/messages" method="POST" timeout=300 result="httpResult" { + httpparam type="header" name="Content-Type" value="application/json"; + httpparam type="header" name="x-api-key" value=CLAUDE_API_KEY; + httpparam type="header" name="anthropic-version" value="2023-06-01"; + httpparam type="body" value=serializeJSON(requestBody); + } - if (httpResult.statusCode != "200 OK" && httpResult.statusCode != 200) { - throw(message="Claude API error: " & httpResult.statusCode, detail=httpResult.fileContent); + httpStatusCode = httpResult.statusCode ?: "0"; + httpFileContent = httpResult.fileContent ?: ""; + + // Normalize status code + if (isNumeric(httpStatusCode)) { + httpStatusCode = int(httpStatusCode); + } else if (findNoCase("200", httpStatusCode)) { + httpStatusCode = 200; + } + + if (httpStatusCode != 200) { + // Try to parse error message from Claude's response + errorMsg = "Claude API error: " & httpResult.statusCode; + try { + errorData = deserializeJSON(httpFileContent); + if (structKeyExists(errorData, "error") && structKeyExists(errorData.error, "message")) { + errorMsg = errorMsg & " - " & errorData.error.message; + } + } catch (any e) { + // Use raw response if can't parse + } + throw(message=errorMsg, detail=httpFileContent); + } + } finally { + // Clean up temp files + if (fileExists(tempRequestFile)) fileDelete(tempRequestFile); } // Parse Claude's response - claudeResponse = deserializeJSON(httpResult.fileContent); + claudeResponse = deserializeJSON(httpFileContent); if (!structKeyExists(claudeResponse, "content") || !arrayLen(claudeResponse.content)) { throw(message="Empty response from Claude"); @@ -266,6 +304,11 @@ Return ONLY the JSON object, no other text."; if (len(e.detail)) { response.DETAIL = e.detail; } + // Add debug info + if (structKeyExists(e, "tagContext") && arrayLen(e.tagContext) > 0) { + response.DEBUG_LINE = e.tagContext[1].line; + response.DEBUG_TEMPLATE = e.tagContext[1].template; + } } writeOutput(serializeJSON(response)); diff --git a/api/setup/testUpload.cfm b/api/setup/testUpload.cfm new file mode 100644 index 0000000..54d1b57 --- /dev/null +++ b/api/setup/testUpload.cfm @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#serializeJSON(response)#