From d9f3ced8b01d375b31ebdacfe09002b05d018804 Mon Sep 17 00:00:00 2001 From: namastex888 Date: Thu, 17 Jul 2025 20:33:16 -0300 Subject: [PATCH] fix: enhance API snippet generation with file upload support and consistent authentication (#9018) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement file upload API snippet generation Add proper two-step file upload support for API code generation: - Detect file tweaks in ChatInput (string), File/VideoFile (path/file_path) - Generate step-based cURL commands with separate copy buttons - Fix ChatInput to use correct /api/v1/run endpoint with tweaks structure - Update JavaScript and Python generators with proper endpoints and payloads - Support dynamic node IDs and processedPayload values - Implement step parsing UI for cURL with proper formatting Co-Authored-By: Automagik Genie * fix: update JavaScript API snippet generator for native Node.js Replace browser-based FormData approach with native Node.js http module - Use built-in fs, http, and path modules instead of browser APIs - Manually construct multipart/form-data payloads - Add proper error handling and HTTP request helpers - Support both ChatInput and File/VideoFile upload scenarios - Maintain authentication header support Co-Authored-By: Automagik Genie * fix: include output_type, input_type, input_value in File component cURL snippets File/VideoFile components were missing the required payload fields when generating cURL commands, causing flows to fail execution. Added the missing fields to match the working pattern used in Chat Input components. Co-Authored-By: Automagik Genie * fix: include output_type, input_type, input_value in Python and JavaScript File component snippets File/VideoFile components were missing the required payload fields in Python and JavaScript generators, matching the fix applied to cURL snippets. Co-Authored-By: Automagik Genie * fix: use 'path' field for File components instead of 'file_id' File components require the 'path' field from v2 upload response as an array, not 'file_id'. Updated all three generators: - Changed file_id to file_path variable extraction - Changed "file_id": value to "path": [value] in tweaks - Updated placeholder text for consistency This fixes the "No files to process" error in File components. Co-Authored-By: Automagik Genie * fix: implement additive file upload API snippet generation - Fixed UI parsing to handle multiple file components correctly - Consolidated upload steps into single step with multiple commands - All file components (ChatInput, File, VideoFile) now work additively - Simplified UI to show only 2 steps: uploads and execution - Improved scrolling for better debugging experience - Removed debug console.log statements * fix: enhance API snippet generation with file upload support and consistent authentication - Add multi-step file upload handling for ChatInput, File, and VideoFile components - Implement automatic session ID generation using UUIDs across all snippets - Ensure API key is always required in generated code (Python, JavaScript, cURL) - Remove unused isAuth parameter from code generation logic - Add comprehensive unit tests for file detection and code generation - Support both Unix/Linux and PowerShell platforms for cURL commands * [autofix.ci] apply automated fixes * 📝 CodeRabbit Chat: Fix curl file upload commands for ChatInput and File/VideoFile APIs * fix: coderabit's feedbacks * [autofix.ci] apply automated fixes * fix: use curl.exe for PowerShell to avoid alias conflicts - Change curl to curl.exe in PowerShell snippets to avoid conflicts with Invoke-WebRequest alias - Remove unused hasChatFiles variable from all code generators - Remove redundant edge case check in curl code generator - Fix Python syntax error (|| to or) - Fix JavaScript crypto.randomUUID() to generate at runtime - Remove unused singleLinePayload variable in Unix branch - Update tests to match new httpModule usage pattern * [autofix.ci] apply automated fixes * fix: remove duplicate API key declaration and add file existence validation - Remove duplicate apiKey declaration from top-level generated code - Keep only the apiKey declaration inside uploadAndExecuteFlow function - Add fs.existsSync() validation before reading files in createFormData - This prevents runtime errors when files don't exist - Maintains 'YOUR_API_KEY_HERE' placeholder as requested * fix: improve hostname/port extraction robustness in JavaScript generator - Replace fragile string splitting with proper URL parsing - Use URL constructor to reliably extract hostname and port - Handle IPv6 addresses, URLs with authentication, and complex hostnames - Apply fix to all three usage locations: ChatInput, File/VideoFile, and execution options - Fallback to default ports (443 for HTTPS, 80 for HTTP) when port not specified * [autofix.ci] apply automated fixes * refactor: streamline code snippet generation for API calls - Remove deprecated step parsing logic in favor of structured data handling - Update getNewCurlCode function to return structured steps for curl commands - Simplify rendering of steps in APITabsComponent to enhance readability and maintainability - Ensure consistent handling of code snippets across different platforms * [autofix.ci] apply automated fixes * test: enhance API snippet generation tests for structured output - Update tests for getNewCurlCode to validate structured steps output - Ensure API key and session_id checks accommodate both string and object return types - Improve clarity and maintainability of test cases for API snippet generation utilities * fix: update session_id handling in curl code generation - Replace dynamic session_id generation with a placeholder "YOUR_SESSION_ID_HERE" for both Unix and PowerShell environments. - Update tests to reflect the change, ensuring they check for the new session_id placeholder instead of UUID generation commands. - Enhance clarity in generated curl commands and maintain consistency across different platforms. * 🔧 (frontend): refactor NodeInputField component to improve readability and maintainability 🔧 (frontend): refactor codeTabs component to add support for auto login feature 🔧 (frontend): refactor api-snippet-generation test to include tests for API key authentication 🔧 (frontend): refactor get-curl-code, get-js-api-code, and get-python-api-code to conditionally include API key based on shouldDisplayApiKey parameter --------- Co-authored-by: Automagik Genie Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: cristhianzl Co-authored-by: Edwin Jose --- .../components/NodeInputField/index.tsx | 4 +- .../modals/apiModal/codeTabs/code-tabs.tsx | 183 +++++-- .../__tests__/api-snippet-generation.test.ts | 484 ++++++++++++++++++ .../apiModal/utils/detect-file-tweaks.ts | 117 +++++ .../modals/apiModal/utils/get-curl-code.tsx | 225 ++++++-- .../modals/apiModal/utils/get-js-api-code.tsx | 304 ++++++++++- .../apiModal/utils/get-python-api-code.tsx | 171 ++++++- 7 files changed, 1371 insertions(+), 117 deletions(-) create mode 100644 src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts create mode 100644 src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx index 8dc268c3b..b00f46c78 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx @@ -119,7 +119,9 @@ export default function NodeInputField({ colors={colors} setFilterEdge={setFilterEdge} showNode={showNode} - testIdComplement={`${data?.type?.toLowerCase()}-${showNode ? "shownode" : "noshownode"}`} + testIdComplement={`${data?.type?.toLowerCase()}-${ + showNode ? "shownode" : "noshownode" + }`} nodeId={data.id} colorName={colorName} /> diff --git a/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx b/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx index e18ab2ad5..630522df6 100644 --- a/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx +++ b/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx @@ -8,10 +8,10 @@ import { useShallow } from "zustand/react/shallow"; import IconComponent from "@/components/common/genericIconComponent"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs-button"; +import { useIsAutoLogin } from "@/hooks/use-is-auto-login"; import useAuthStore from "@/stores/authStore"; import useFlowStore from "@/stores/flowStore"; import { useTweaksStore } from "@/stores/tweaksStore"; -import { tabsArrayType } from "@/types/tabs"; import { hasStreaming } from "@/utils/reactflowUtils"; import { getOS } from "@/utils/utils"; import { useDarkStore } from "../../../stores/darkStore"; @@ -34,7 +34,8 @@ const operatingSystemTabs = [ ]; export default function APITabsComponent() { - const [isCopied, setIsCopied] = useState(false); + const [isCopied, setIsCopied] = useState(false); + const [copiedStep, setCopiedStep] = useState(null); const endpointName = useFlowStore( useShallow((state) => state.currentFlow?.endpoint_name), ); @@ -87,18 +88,28 @@ export default function APITabsComponent() { )?.name || "macoslinux", ); + const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const isAutoLogin = useIsAutoLogin(); + const shouldDisplayApiKey = isAuthenticated && !isAutoLogin; + const tabsList = [ { title: "Python", icon: "BWPython", language: "python", - code: getNewPythonApiCode(codeOptions), + code: getNewPythonApiCode({ + ...codeOptions, + shouldDisplayApiKey, + }), }, { title: "JavaScript", icon: "javascript", language: "javascript", - code: getNewJsApiCode(codeOptions), + code: getNewJsApiCode({ + ...codeOptions, + shouldDisplayApiKey, + }), }, { title: "cURL", @@ -107,31 +118,41 @@ export default function APITabsComponent() { code: getNewCurlCode({ ...codeOptions, platform: selectedPlatform === "windows" ? "powershell" : "unix", + shouldDisplayApiKey, }), }, ]; const [selectedTab, setSelectedTab] = useState("Python"); - const copyToClipboard = () => { + const copyToClipboard = (codeText?: string, stepId?: string) => { if (!navigator.clipboard || !navigator.clipboard.writeText) { return; } const currentTab = tabsList.find((tab) => tab.title === selectedTab); - if (currentTab) { - navigator.clipboard.writeText(currentTab.code).then(() => { - setIsCopied(true); - - setTimeout(() => { - setIsCopied(false); - }, 2000); + const textToCopy = + codeText || (typeof currentTab?.code === "string" ? currentTab.code : ""); + if (textToCopy) { + navigator.clipboard.writeText(textToCopy).then(() => { + if (stepId) { + setCopiedStep(stepId); + setTimeout(() => { + setCopiedStep(null); + }, 2000); + } else { + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 2000); + } }); } }; useEffect(() => { setIsCopied(false); + setCopiedStep(null); }, [selectedTab, selectedPlatform]); const currentTab = tabsList.find((tab) => tab.title === selectedTab); @@ -180,40 +201,110 @@ export default function APITabsComponent() { )} {/* Code content */} - {currentTab && ( -
-
- - - {currentTab.code} - -
-
- )} + {currentTab && + (() => { + // Work directly with structured data - no parsing needed + const codeData = currentTab.code; + const hasSteps = + typeof codeData === "object" && + codeData !== null && + "steps" in codeData; + + if (hasSteps) { + const steps = ( + codeData as { steps: { title: string; code: string }[] } + ).steps; + return ( +
+ {steps.map((step, index) => ( +
+

{step.title}

+
+ + 0} + wrapLongLines={true} + language={currentTab.language} + style={dark ? oneDark : oneLight} + className={`!mt-0 ${ + index === steps.length - 1 ? "h-full" : "" + } w-full overflow-scroll !rounded-b-md border border-border text-left !custom-scroll`} + > + {step.code} + +
+
+ ))} +
+ ); + } else { + return ( +
+
+ + + {currentTab.code} + +
+
+ ); + } + })()} ); diff --git a/src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts b/src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts new file mode 100644 index 000000000..a0f33d77d --- /dev/null +++ b/src/frontend/src/modals/apiModal/utils/__tests__/api-snippet-generation.test.ts @@ -0,0 +1,484 @@ +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getChatInputNodeId, + getFileNodeId, + getNonFileTypeTweaks, + hasChatInputFiles, + hasFileTweaks, +} from "../detect-file-tweaks"; +import { getNewCurlCode } from "../get-curl-code"; +import { getNewJsApiCode } from "../get-js-api-code"; +import { getNewPythonApiCode } from "../get-python-api-code"; + +describe("API Snippet Generation Utilities", () => { + describe("File Tweak Detection", () => { + it("should detect File path array", () => { + expect(hasFileTweaks({ node1: { path: [] } })).toBe(true); + }); + + it("should detect VideoFile file_path string", () => { + expect(hasFileTweaks({ node2: { file_path: "path" } })).toBe(true); + }); + + it("should detect ChatInput files string", () => { + expect(hasFileTweaks({ node3: { files: "path" } })).toBe(true); + }); + + it("should return false for no files", () => { + expect(hasFileTweaks({ node: { other: "value" } })).toBe(false); + }); + + it("should detect ChatInput files specifically", () => { + expect(hasChatInputFiles({ node: { files: "path" } })).toBe(true); + expect(hasChatInputFiles({ node: { path: [] } })).toBe(false); + }); + + it("should get single ChatInput node ID", () => { + expect(getChatInputNodeId({ node1: { files: "path" } })).toBe("node1"); + expect(getChatInputNodeId({ node1: { other: "value" } })).toBeNull(); + }); + + it("should get single File node ID", () => { + expect(getFileNodeId({ node1: { path: [] } })).toBe("node1"); + expect(getFileNodeId({ node1: { file_path: "path" } })).toBe("node1"); + expect(getFileNodeId({ node1: { files: "path" } })).toBeNull(); + }); + + it("should get all ChatInput node IDs", () => { + expect( + getAllChatInputNodeIds({ + node1: { files: "p1" }, + node2: { files: "p2" }, + node3: { other: "v" }, + }), + ).toEqual(["node1", "node2"]); + }); + + it("should get all File and VideoFile node IDs", () => { + expect( + getAllFileNodeIds({ + node1: { path: [] }, + node2: { file_path: "p" }, + node3: { files: "p" }, + node4: { other: "v" }, + }), + ).toEqual(["node1", "node2"]); + }); + + it("should filter out file-related tweaks", () => { + const tweaks = { + node1: { path: [] }, + node2: { file_path: "p" }, + node3: { files: "p" }, + node4: { other: "value" }, + node5: { param: 123 }, + }; + const result = getNonFileTypeTweaks(tweaks); + expect(result).toEqual({ + node4: { other: "value" }, + node5: { param: 123 }, + }); + }); + }); + + describe("API Code Generation", () => { + const baseOptions = { + flowId: "test-flow-id", + endpointName: "test-endpoint", + processedPayload: { + output_type: "chat", + input_type: "chat", + input_value: "Hello", + }, + shouldDisplayApiKey: true, + }; + + const noAuthOptions = { + ...baseOptions, + shouldDisplayApiKey: false, + }; + + describe("Python Code Generation", () => { + it("should generate basic Python code with API key authentication", () => { + const code = getNewPythonApiCode(baseOptions); + + // Check for required imports + expect(code).toContain("import requests"); + expect(code).toContain("import uuid"); + + // Check for API key + expect(code).toContain("api_key = 'YOUR_API_KEY_HERE'"); + expect(code).toContain('headers = {"x-api-key": api_key}'); + + // Check for session_id + expect(code).toContain('payload["session_id"] = str(uuid.uuid4())'); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate basic Python code without API key authentication", () => { + const code = getNewPythonApiCode(noAuthOptions); + + // Check for required imports + expect(code).toContain("import requests"); + expect(code).toContain("import uuid"); + + // Should not contain API key + expect(code).not.toContain("api_key = 'YOUR_API_KEY_HERE'"); + expect(code).not.toContain('headers = {"x-api-key": api_key}'); + + // Check for session_id + expect(code).toContain('payload["session_id"] = str(uuid.uuid4())'); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate multi-step code for file uploads with authentication", () => { + const optionsWithFiles = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewPythonApiCode(optionsWithFiles); + + // Check for API key in file upload sections + expect(code).toContain("api_key = 'YOUR_API_KEY_HERE'"); + expect(code).toContain('headers = {"x-api-key": api_key}'); + + // Check for file upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + expect(code).toContain("with open"); + expect(code).toContain('files={"file": f}'); + }); + + it("should generate multi-step code for file uploads without authentication", () => { + const optionsWithFiles = { + ...noAuthOptions, + processedPayload: { + ...noAuthOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewPythonApiCode(optionsWithFiles); + + // Should not contain API key + expect(code).not.toContain("api_key = 'YOUR_API_KEY_HERE'"); + + // Check for file upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + expect(code).toContain("with open"); + expect(code).toContain('files={"file": f}'); + }); + }); + + describe("JavaScript Code Generation", () => { + it("should generate basic JavaScript code with API key authentication", () => { + const code = getNewJsApiCode(baseOptions); + + // Check for API key + expect(code).toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + expect(code).toContain('"x-api-key": apiKey'); + + // Check for session_id + expect(code).toContain("session_id"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate basic JavaScript code without API key authentication", () => { + const code = getNewJsApiCode(noAuthOptions); + + // Should not contain API key + expect(code).not.toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + expect(code).not.toContain("'x-api-key': apiKey"); + + // Check for session_id + expect(code).toContain("session_id"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate multi-step code for file uploads with authentication", () => { + const optionsWithFiles = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewJsApiCode(optionsWithFiles); + + // Check for required modules + expect(code).toContain("const fs = require('fs')"); + expect(code).toContain( + "httpModule = protocol === 'https:' ? require('https') : require('http')", + ); + + // Check for API key + expect(code).toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + expect(code).toContain("const authHeaders = { 'x-api-key': apiKey }"); + + // Check for file upload functions + expect(code).toContain("createFormData"); + expect(code).toContain("makeRequest"); + + // Check for upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + }); + + it("should generate multi-step code for file uploads without authentication", () => { + const optionsWithFiles = { + ...noAuthOptions, + processedPayload: { + ...noAuthOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const code = getNewJsApiCode(optionsWithFiles); + + // Check for required modules + expect(code).toContain("const fs = require('fs')"); + expect(code).toContain( + "httpModule = protocol === 'https:' ? require('https') : require('http')", + ); + + // Should not contain API key + expect(code).not.toContain("const apiKey = 'YOUR_API_KEY_HERE'"); + + // Check for file upload functions + expect(code).toContain("createFormData"); + expect(code).toContain("makeRequest"); + + // Check for upload steps + expect(code).toContain("/api/v1/files/upload/"); + expect(code).toContain("/api/v2/files"); + }); + }); + + describe("cURL Code Generation", () => { + it("should generate Unix cURL code with API key authentication", () => { + const code = getNewCurlCode({ ...baseOptions, platform: "unix" }); + + // Check for API key + expect(code).toContain("x-api-key: YOUR_API_KEY_HERE"); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate Unix cURL code without API key authentication", () => { + const code = getNewCurlCode({ ...noAuthOptions, platform: "unix" }); + + // Should not contain API key + expect(code).not.toContain("x-api-key: YOUR_API_KEY_HERE"); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate PowerShell cURL code with API key authentication", () => { + const code = getNewCurlCode({ ...baseOptions, platform: "powershell" }); + + // Check for API key + expect(code).toContain('--header "x-api-key: YOUR_API_KEY_HERE"'); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate PowerShell cURL code without API key authentication", () => { + const code = getNewCurlCode({ + ...noAuthOptions, + platform: "powershell", + }); + + // Should not contain API key + expect(code).not.toContain('--header "x-api-key: YOUR_API_KEY_HERE"'); + + // Check for session_id placeholder + expect(code).toContain("YOUR_SESSION_ID_HERE"); + + // Check for correct endpoint + expect(code).toContain("/api/v1/run/test-endpoint"); + }); + + it("should generate multi-step code for file uploads with authentication", () => { + const optionsWithFiles = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const result = getNewCurlCode({ + ...optionsWithFiles, + platform: "unix", + }) as { steps: { title: string; code: string }[] }; + + // Check that it returns structured steps + expect(result).toHaveProperty("steps"); + expect(Array.isArray(result.steps)).toBe(true); + expect(result.steps).toHaveLength(2); + + // Check step 1 (upload files) + expect(result.steps[0]).toHaveProperty("title"); + expect(result.steps[0].title).toContain("Upload files"); + expect(result.steps[0]).toHaveProperty("code"); + expect(result.steps[0].code).toContain("/api/v1/files/upload/"); + expect(result.steps[0].code).toContain("/api/v2/files"); + expect(result.steps[0].code).toContain('--form "file=@'); + expect(result.steps[0].code).toContain("x-api-key: YOUR_API_KEY_HERE"); + + // Check step 2 (execute flow) + expect(result.steps[1]).toHaveProperty("title"); + expect(result.steps[1].title).toContain("Execute"); + expect(result.steps[1]).toHaveProperty("code"); + expect(result.steps[1].code).toContain("/api/v1/run/test-endpoint"); + expect(result.steps[1].code).toContain("x-api-key: YOUR_API_KEY_HERE"); + }); + + it("should generate multi-step code for file uploads without authentication", () => { + const optionsWithFiles = { + ...noAuthOptions, + processedPayload: { + ...noAuthOptions.processedPayload, + tweaks: { + node1: { files: "chat_input.txt" }, + node2: { path: ["file.pdf"] }, + }, + }, + }; + + const result = getNewCurlCode({ + ...optionsWithFiles, + platform: "unix", + }) as { steps: { title: string; code: string }[] }; + + // Check that it returns structured steps + expect(result).toHaveProperty("steps"); + expect(Array.isArray(result.steps)).toBe(true); + expect(result.steps).toHaveLength(2); + + // Check step 1 (upload files) - should not contain API key + expect(result.steps[0]).toHaveProperty("title"); + expect(result.steps[0].title).toContain("Upload files"); + expect(result.steps[0]).toHaveProperty("code"); + expect(result.steps[0].code).toContain("/api/v1/files/upload/"); + expect(result.steps[0].code).toContain("/api/v2/files"); + expect(result.steps[0].code).toContain('--form "file=@'); + expect(result.steps[0].code).not.toContain( + "x-api-key: YOUR_API_KEY_HERE", + ); + + // Check step 2 (execute flow) - should not contain API key + expect(result.steps[1]).toHaveProperty("title"); + expect(result.steps[1].title).toContain("Execute"); + expect(result.steps[1]).toHaveProperty("code"); + expect(result.steps[1].code).toContain("/api/v1/run/test-endpoint"); + expect(result.steps[1].code).not.toContain( + "x-api-key: YOUR_API_KEY_HERE", + ); + }); + }); + + describe("Common Features", () => { + it("should conditionally include API key based on shouldDisplayApiKey parameter", () => { + const pythonCodeAuth = getNewPythonApiCode(baseOptions); + const pythonCodeNoAuth = getNewPythonApiCode(noAuthOptions); + const jsCodeAuth = getNewJsApiCode(baseOptions); + const jsCodeNoAuth = getNewJsApiCode(noAuthOptions); + const curlResultAuth = getNewCurlCode(baseOptions); + const curlResultNoAuth = getNewCurlCode(noAuthOptions); + + // With authentication + expect(pythonCodeAuth).toContain("YOUR_API_KEY_HERE"); + expect(jsCodeAuth).toContain("YOUR_API_KEY_HERE"); + expect(curlResultAuth).toContain("YOUR_API_KEY_HERE"); + + // Without authentication + expect(pythonCodeNoAuth).not.toContain("YOUR_API_KEY_HERE"); + expect(jsCodeNoAuth).not.toContain("YOUR_API_KEY_HERE"); + expect(curlResultNoAuth).not.toContain("YOUR_API_KEY_HERE"); + }); + + it("should include session_id in all generators", () => { + const pythonCode = getNewPythonApiCode(baseOptions); + const jsCode = getNewJsApiCode(baseOptions); + const curlResult = getNewCurlCode(baseOptions); + + expect(pythonCode).toContain("session_id"); + expect(jsCode).toContain("session_id"); + + // Handle both string and object return types for cURL + if (typeof curlResult === "string") { + expect(curlResult).toContain("session_id"); + } else { + // For steps object, check that at least one step contains session_id + const hasSessionId = curlResult.steps.some((step) => + step.code.includes("session_id"), + ); + expect(hasSessionId).toBe(true); + } + }); + + it("should handle empty tweaks correctly", () => { + const optionsWithEmptyTweaks = { + ...baseOptions, + processedPayload: { + ...baseOptions.processedPayload, + tweaks: {}, + }, + }; + + const pythonCode = getNewPythonApiCode(optionsWithEmptyTweaks); + const jsCode = getNewJsApiCode(optionsWithEmptyTweaks); + const curlCode = getNewCurlCode(optionsWithEmptyTweaks); + + // Should not generate file upload steps + expect(pythonCode).not.toContain("Step 1:"); + expect(jsCode).not.toContain("Step 1:"); + // cURL should return string, not steps object + expect(typeof curlCode).toBe("string"); + }); + }); + }); +}); diff --git a/src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts b/src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts new file mode 100644 index 000000000..c5156c7fc --- /dev/null +++ b/src/frontend/src/modals/apiModal/utils/detect-file-tweaks.ts @@ -0,0 +1,117 @@ +/** Checks if the tweaks object contains any file-related fields (path for File, file_path for VideoFile, files for ChatInput). */ +export function hasFileTweaks(tweaks: Record): boolean { + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + // File component: { path: [...] } + if ("path" in tweak && Array.isArray(tweak.path)) return true; + + // Video File: { file_path: "..." } + if ("file_path" in tweak && typeof tweak.file_path === "string") + return true; + + // Chat Input: { files: "..." } - files is a string field + if ("files" in tweak && typeof tweak.files === "string") return true; + } + + return false; +} + +/** Checks specifically for ChatInput files field (v1 API). */ +export function hasChatInputFiles(tweaks: Record): boolean { + return Object.values(tweaks).some( + (tweak) => + tweak && + typeof tweak === "object" && + "files" in tweak && + typeof tweak.files === "string", + ); +} + +/** Gets node ID for single ChatInput with files (v1). Returns null if none. */ +export function getChatInputNodeId(tweaks: Record): string | null { + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + if ("files" in tweak && typeof tweak.files === "string") { + return nodeId; + } + } + + return null; +} + +/** Gets node ID for single File/VideoFile (v2). Returns null if none. */ +export function getFileNodeId(tweaks: Record): string | null { + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + // File component: { path: [...] } + if ("path" in tweak && Array.isArray(tweak.path)) return nodeId; + + // Video File: { file_path: "..." } + if ("file_path" in tweak && typeof tweak.file_path === "string") + return nodeId; + } + + return null; +} + +/** Gets all node IDs for ChatInputs with files (v1). */ +export function getAllChatInputNodeIds(tweaks: Record): string[] { + const nodeIds: string[] = []; + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + if ("files" in tweak && typeof tweak.files === "string") { + nodeIds.push(nodeId); + } + } + + return nodeIds; +} + +/** Gets all node IDs for File/VideoFile components (v2). */ +export function getAllFileNodeIds(tweaks: Record): string[] { + const nodeIds: string[] = []; + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") continue; + + // File component: { path: [...] } + if ("path" in tweak && Array.isArray(tweak.path)) { + nodeIds.push(nodeId); + } + + // Video File: { file_path: "..." } + if ("file_path" in tweak && typeof tweak.file_path === "string") { + nodeIds.push(nodeId); + } + } + + return nodeIds; +} + +/** Filters out file-related tweaks, returning only non-file ones. */ +export function getNonFileTypeTweaks( + tweaks: Record, +): Record { + const nonFileTweaks: Record = {}; + for (const [nodeId, tweak] of Object.entries(tweaks)) { + if (!tweak || typeof tweak !== "object") { + nonFileTweaks[nodeId] = tweak; + continue; + } + + // Skip file-related tweaks + const isFileComponent = + ("path" in tweak && Array.isArray(tweak.path)) || + ("file_path" in tweak && typeof tweak.file_path === "string") || + ("files" in tweak && typeof tweak.files === "string"); + + if (!isFileComponent) { + nonFileTweaks[nodeId] = tweak; + } + } + + return nonFileTweaks; +} diff --git a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx index 1fec15953..140222693 100644 --- a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx @@ -1,6 +1,12 @@ import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags"; import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol"; -import type { GetCodeType } from "@/types/tweaks"; +import { GetCodeType } from "@/types/tweaks"; +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getNonFileTypeTweaks, + hasFileTweaks, +} from "./detect-file-tweaks"; /** * Generates a cURL command for making a POST request to a webhook endpoint. @@ -18,7 +24,9 @@ export function getCurlWebhookCode({ format = "multiline", }: GetCodeType & { format?: "multiline" | "singleline" }) { const { protocol, host } = customGetHostProtocol(); - const baseUrl = `${protocol}//${host}/api/v1/webhook/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}/api/v1/webhook/${ + endpointName || flowId + }`; const authHeader = !isAuth ? `-H 'x-api-key: '` : ""; if (format === "singleline") { @@ -38,19 +46,23 @@ export function getCurlWebhookCode({ `.trim(); } +/** Generates Curl command for API calls, handling multi-step file uploads (v1 API for ChatInput files, v2 for File/VideoFile) before execution if tweaks contain files. Supports Unix/PowerShell and optional auth. */ export function getNewCurlCode({ flowId, endpointName, processedPayload, platform, + shouldDisplayApiKey, }: { flowId: string; endpointName: string; processedPayload: any; platform?: "unix" | "powershell"; -}): string { + shouldDisplayApiKey: boolean; +}): { steps: { title: string; code: string }[] } | string { const { protocol, host } = customGetHostProtocol(); - const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}`; + const apiUrl = `${baseUrl}/api/v1/run/${endpointName || flowId}`; // Auto-detect if no platform specified const detectedPlatform = @@ -59,41 +71,196 @@ export function getNewCurlCode({ ? "powershell" : "unix"); - const singleLinePayload = JSON.stringify(processedPayload); + // Check if there are file uploads + const tweaks = processedPayload.tweaks || {}; + const hasFiles = hasFileTweaks(tweaks); - if (detectedPlatform === "powershell") { - // PowerShell with here-string (most robust for complex JSON) - return `if (-not $env:LANGFLOW_API_KEY) { - Write-Error "LANGFLOW_API_KEY environment variable not found" - exit 1 -} + // If no file uploads, use existing logic + if (!hasFiles) { + if (detectedPlatform === "powershell") { + const payloadWithSession = { + ...processedPayload, + session_id: "YOUR_SESSION_ID_HERE", + }; + const singleLinePayload = JSON.stringify(payloadWithSession); + // PowerShell with here-string (most robust for complex JSON) + const authHeader = shouldDisplayApiKey + ? ` --header "x-api-key: YOUR_API_KEY_HERE" \`` + : ""; -$jsonData = @' + return `$jsonData = @' ${singleLinePayload} '@ -curl --request POST \` +curl.exe --request POST \` --url "${apiUrl}?stream=false" \` - --header "Content-Type: application/json" \` - --header "x-api-key: $env:LANGFLOW_API_KEY" \` + --header "Content-Type: application/json" \`${ + authHeader ? "\n" + authHeader : "" + } --data $jsonData`; - } else { - // Unix-like systems (Linux, Mac, WSL2) - const unixFormattedPayload = JSON.stringify(processedPayload, null, 2) - .split("\n") - .map((line, index) => (index === 0 ? line : " " + line)) - .join("\n\t\t"); + } else { + const payloadWithSession = { + ...processedPayload, + session_id: "YOUR_SESSION_ID_HERE", + }; + // Unix-like systems (Linux, Mac, WSL2) + const unixFormattedPayload = JSON.stringify(payloadWithSession, null, 2) + .split("\n") + .map((line, index) => (index === 0 ? line : " " + line)) + .join("\n\t\t"); - return `# Get API key from environment variable -if [ -z "$LANGFLOW_API_KEY" ]; then - echo "Error: LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables." - exit 1 -fi + const authHeader = shouldDisplayApiKey + ? ` --header "x-api-key: YOUR_API_KEY_HERE" \\ ` + : ""; -curl --request POST \\ + return `curl --request POST \\ --url '${apiUrl}?stream=false' \\ - --header 'Content-Type: application/json' \\ - --header "x-api-key: $LANGFLOW_API_KEY" \\ + --header 'Content-Type: application/json' \\${ + authHeader ? "\n" + authHeader : "" + } --data '${unixFormattedPayload}'`; + } + } + + // File upload logic - handle multiple file types additively + const chatInputNodeIds = getAllChatInputNodeIds(tweaks); + const fileNodeIds = getAllFileNodeIds(tweaks); + const nonFileTweaks = getNonFileTypeTweaks(tweaks); + + // Build upload commands and tweak entries + const uploadCommands: string[] = []; + const tweakEntries: string[] = []; + let uploadCounter = 1; + + // Add ChatInput file uploads (v1 API) + chatInputNodeIds.forEach((nodeId, index) => { + if (detectedPlatform === "powershell") { + uploadCommands.push( + `curl.exe --request POST \` + --url "${baseUrl}/api/v1/files/upload/${flowId}" \` + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_image_${uploadCounter}.jpg"`, + ); + } else { + uploadCommands.push( + `curl --request POST \\ + --url "${baseUrl}/api/v1/files/upload/${flowId}" \\ + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_image_${uploadCounter}.jpg"`, + ); + } + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + modifiedTweak.files = [ + `REPLACE_WITH_FILE_PATH_FROM_UPLOAD_${uploadCounter}`, + ]; + const tweakEntry = ` "${nodeId}": ${JSON.stringify( + modifiedTweak, + null, + 6, + ) + .split("\n") + .join("\n ")}`; + tweakEntries.push(tweakEntry); + uploadCounter++; + }); + + // Add File/VideoFile uploads (v2 API) + fileNodeIds.forEach((nodeId, index) => { + if (detectedPlatform === "powershell") { + uploadCommands.push( + `curl.exe --request POST \` + --url "${baseUrl}/api/v2/files" \` + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_file_${uploadCounter}.pdf"`, + ); + } else { + uploadCommands.push( + `curl --request POST \\ + --url "${baseUrl}/api/v2/files" \\ + ${shouldDisplayApiKey ? '--header "x-api-key: YOUR_API_KEY_HERE" \\' : ""} + --form "file=@your_file_${uploadCounter}.pdf"`, + ); + } + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + if ("path" in originalTweak) { + modifiedTweak.path = [ + `REPLACE_WITH_FILE_PATH_FROM_UPLOAD_${uploadCounter}`, + ]; + } else if ("file_path" in originalTweak) { + modifiedTweak.file_path = `REPLACE_WITH_FILE_PATH_FROM_UPLOAD_${uploadCounter}`; + } + const tweakEntry = ` "${nodeId}": ${JSON.stringify( + modifiedTweak, + null, + 6, + ) + .split("\n") + .join("\n ")}`; + tweakEntries.push(tweakEntry); + uploadCounter++; + }); + + // Add non-file tweaks + Object.entries(nonFileTweaks).forEach(([nodeId, tweak]) => { + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(tweak, null, 6) + .split("\n") + .join("\n ")}`, + ); + }); + + const allTweaks = tweakEntries.length > 0 ? tweakEntries.join(",\n") : ""; + + if (detectedPlatform === "powershell") { + const authHeader = shouldDisplayApiKey + ? ` -H "x-api-key: YOUR_API_KEY_HERE"` + : ""; + + const uploadStep = uploadCommands.join("\n"); + const executeStep = `curl.exe -X POST "${apiUrl}" -H "Content-Type: application/json"${authHeader} -d '{ + "output_type": "${processedPayload.output_type || "chat"}", + "input_type": "${processedPayload.input_type || "chat"}", + "input_value": "${processedPayload.input_value || "Your message here"}", + "session_id": "YOUR_SESSION_ID_HERE", + "tweaks": { +${allTweaks} + } +}'`; + + // Return structured steps instead of concatenated string + return { + steps: [ + { title: "Upload files to the server", code: uploadStep }, + { title: "Execute the flow with uploaded files", code: executeStep }, + ], + }; + } else { + const authHeader = shouldDisplayApiKey + ? ` -H "x-api-key: YOUR_API_KEY_HERE"` + : ""; + + const uploadStep = uploadCommands.join("\n"); + const executeStep = `curl -X POST \\ + "${apiUrl}" \\ + -H "Content-Type: application/json"${ + authHeader ? " \\\n " + authHeader : "" + } \\ + -d '{\n "output_type": "${ + processedPayload.output_type || "chat" + }",\n "input_type": "${ + processedPayload.input_type || "chat" + }",\n "input_value": "${ + processedPayload.input_value || "Your message here" + }",\n "session_id": "YOUR_SESSION_ID_HERE",\n "tweaks": {\n${allTweaks}\n }\n }'`; + + // Return structured steps instead of concatenated string + return { + steps: [ + { title: "Upload files to the server", code: uploadStep }, + { title: "Execute the flow with uploaded files", code: executeStep }, + ], + }; } } diff --git a/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx b/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx index cc9ca17fb..8b13fe155 100644 --- a/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx @@ -1,46 +1,64 @@ import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol"; +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getChatInputNodeId, + getFileNodeId, + getNonFileTypeTweaks, + hasChatInputFiles, + hasFileTweaks, +} from "./detect-file-tweaks"; -/** - * Generates JavaScript code for making API calls to a Langflow endpoint. - * - * @param {Object} params - The parameters for generating the API code - * @param {string} params.flowId - The ID of the flow to run - * @param {string} params.endpointName - The endpoint name for the flow - * @param {Object} params.processedPayload - The pre-processed payload object - * @returns {string} Generated JavaScript code as a string - */ +/** Generates Node.js code for API calls, with multi-step file uploads (v1 for ChatInput, v2 for File/VideoFile) using http module, then flow execution. Handles auth. */ export function getNewJsApiCode({ flowId, endpointName, processedPayload, + shouldDisplayApiKey, }: { flowId: string; endpointName: string; processedPayload: any; + shouldDisplayApiKey: boolean; }): string { const { protocol, host } = customGetHostProtocol(); - const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}`; - // Add session_id to payload - const payloadWithSession = { - ...processedPayload, - session_id: "user_1", // Optional: Use session tracking if needed - }; + // Parse URL for robust hostname/port extraction + const parsedUrl = new URL(baseUrl); + const hostname = parsedUrl.hostname; + const port = + parsedUrl.port || (parsedUrl.protocol === "https:" ? "443" : "80"); - const payloadString = JSON.stringify(payloadWithSession, null, 4); + // Check if there are file uploads + const tweaks = processedPayload.tweaks || {}; + const hasFiles = hasFileTweaks(tweaks); - return `// Get API key from environment variable -if (!process.env.LANGFLOW_API_KEY) { - throw new Error('LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables.'); -} + // If no file uploads, use existing logic + if (!hasFiles) { + const apiUrl = `${baseUrl}/api/v1/run/${endpointName || flowId}`; -const payload = ${payloadString}; + const payloadString = JSON.stringify(processedPayload, null, 4); + + const authSection = shouldDisplayApiKey + ? `const crypto = require('crypto'); +const apiKey = 'YOUR_API_KEY_HERE'; +` + : ""; + + const headersSection = shouldDisplayApiKey + ? ` headers: { + 'Content-Type': 'application/json', + "x-api-key": apiKey + },` + : ""; + + return `${authSection}const payload = ${payloadString}; +payload.session_id = crypto.randomUUID(); const options = { method: 'POST', - headers: { - 'Content-Type': 'application/json',\n "x-api-key": process.env.LANGFLOW_API_KEY - }, +${headersSection} body: JSON.stringify(payload) }; @@ -48,4 +66,242 @@ fetch('${apiUrl}', options) .then(response => response.json()) .then(response => console.warn(response)) .catch(err => console.error(err));`; + } + + // File upload logic - handle multiple file types additively + const chatInputNodeIds = getAllChatInputNodeIds(tweaks); + const fileNodeIds = getAllFileNodeIds(tweaks); + const nonFileTweaks = getNonFileTypeTweaks(tweaks); + + if (chatInputNodeIds.length === 0 && fileNodeIds.length === 0) { + return getNewJsApiCode({ + flowId, + endpointName, + processedPayload: { ...processedPayload, tweaks: nonFileTweaks }, + shouldDisplayApiKey, + }); + } + + const authSection = shouldDisplayApiKey + ? `const apiKey = 'YOUR_API_KEY_HERE'; +const authHeaders = { 'x-api-key': apiKey };` + : ""; + + // Build upload steps for each file component + const uploadSteps: string[] = []; + const resultVariables: string[] = []; + const tweakEntries: string[] = []; + + // ChatInput files (v1 API) + chatInputNodeIds.forEach((nodeId, index) => { + const varName = `chatFilePath${index + 1}`; + resultVariables.push(varName); + + uploadSteps.push(` // Step ${ + uploadSteps.length + 1 + }: Upload file for ChatInput ${nodeId} + const { payload: chatPayload${index + 1}, boundary: chatBoundary${ + index + 1 + } } = createFormData('your_image_${index + 1}.jpg'); + + const chatUploadOptions${index + 1} = { + hostname: '${hostname}', + port: ${port}, + path: \`/api/v1/files/upload/\${FLOW_ID}\`, + method: 'POST', + headers: { + 'Content-Type': \`multipart/form-data; boundary=\${chatBoundary${ + index + 1 + }}\`, + 'Content-Length': chatPayload${index + 1}.length, + ...authHeaders + } + }; + + const chatUploadResult${ + index + 1 + } = await makeRequest(chatUploadOptions${index + 1}, chatPayload${ + index + 1 + }); + const ${varName} = chatUploadResult${index + 1}.file_path; + console.log('ChatInput upload ${ + index + 1 + } successful! File path:', ${varName});`); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + modifiedTweak.files = [varName]; + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(modifiedTweak, null, 12) + .split("\n") + .join("\n ")}`, + ); + }); + + // File/VideoFile components (v2 API) + fileNodeIds.forEach((nodeId, index) => { + const varName = `filePath${index + 1}`; + resultVariables.push(varName); + + uploadSteps.push(` // Step ${ + uploadSteps.length + 1 + }: Upload file for File/VideoFile ${nodeId} + const { payload: filePayload${index + 1}, boundary: fileBoundary${ + index + 1 + } } = createFormData('your_file_${index + 1}.pdf'); + + const fileUploadOptions${index + 1} = { + hostname: '${hostname}', + port: ${port}, + path: '/api/v2/files', + method: 'POST', + headers: { + 'Content-Type': \`multipart/form-data; boundary=\${fileBoundary${ + index + 1 + }}\`, + 'Content-Length': filePayload${index + 1}.length, + ...authHeaders + } + }; + + const fileUploadResult${ + index + 1 + } = await makeRequest(fileUploadOptions${index + 1}, filePayload${ + index + 1 + }); + const ${varName} = fileUploadResult${index + 1}.path; + console.log('File upload ${ + index + 1 + } successful! File path:', ${varName});`); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + if ("path" in originalTweak) { + modifiedTweak.path = [varName]; + } else if ("file_path" in originalTweak) { + modifiedTweak.file_path = varName; + } + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(modifiedTweak, null, 12) + .split("\n") + .join("\n ")}`, + ); + }); + + // Add non-file tweaks + Object.entries(nonFileTweaks).forEach(([nodeId, tweak]) => { + tweakEntries.push( + ` "${nodeId}": ${JSON.stringify(tweak, null, 12) + .split("\n") + .join("\n ")}`, + ); + }); + + const allTweaks = tweakEntries.length > 0 ? tweakEntries.join(",\n") : ""; + + return `${authSection}const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const BASE_URL = "${baseUrl}"; +const FLOW_ID = "${flowId}"; +const protocol = new URL(BASE_URL).protocol; +const httpModule = protocol === 'https:' ? require('https') : require('http'); + +// Helper function to create multipart form data +function createFormData(filePath) { + const boundary = '----FormBoundary' + Date.now(); + const filename = path.basename(filePath); + + if (!fs.existsSync(filePath)) { + throw new Error(\`File not found: \${filePath}\`); + } + + const fileData = fs.readFileSync(filePath); + + let data = ''; + data += \`--\${boundary}\\r\\n\`; + data += \`Content-Disposition: form-data; name="file"; filename="\${filename}"\\r\\n\`; + data += \`Content-Type: application/octet-stream\\r\\n\\r\\n\`; + + const payload = Buffer.concat([ + Buffer.from(data, 'utf8'), + fileData, + Buffer.from(\`\\r\\n--\${boundary}--\\r\\n\`, 'utf8') + ]); + + return { payload, boundary }; +} + +// Helper function to make HTTP requests +function makeRequest(options, data) { + return new Promise((resolve, reject) => { + const req = httpModule.request(options, (res) => { + let responseData = ''; + res.on('data', (chunk) => { responseData += chunk; }); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + resolve(JSON.parse(responseData)); + } catch (e) { + resolve(responseData); + } + } else { + reject(new Error(\`Request failed with status \${res.statusCode}: \${responseData}\`)); + } + }); + }); + req.on('error', reject); + if (data) req.write(data); + req.end(); + }); +} + +async function uploadAndExecuteFlow() { + try {${ + shouldDisplayApiKey + ? ` + const apiKey = 'YOUR_API_KEY_HERE'; + const authHeaders = { 'x-api-key': apiKey };` + : ` + const authHeaders = {};` + } + +${uploadSteps.join("\n\n")} + + // Step ${uploadSteps.length + 1}: Execute flow with all file paths + const executePayload = JSON.stringify({ + "output_type": "${processedPayload.output_type || "chat"}", + "input_type": "${processedPayload.input_type || "chat"}", + "input_value": "${ + processedPayload.input_value || "Your message here" + }", + "session_id": crypto.randomUUID(), + "tweaks": { +${allTweaks} + } + }); + + const executeOptions = { + hostname: '${hostname}', + port: ${port}, + path: \`/api/v1/run/${endpointName || flowId}\`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(executePayload), + ...authHeaders + } + }; + + const result = await makeRequest(executeOptions, executePayload); + console.log('Flow execution successful!'); + console.log(result); + + } catch (error) { + console.error('Error:', error.message); + } +} + +uploadAndExecuteFlow();`; } diff --git a/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx b/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx index b76d56a8e..8c54f3b42 100644 --- a/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx @@ -1,40 +1,60 @@ import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol"; +import { + getAllChatInputNodeIds, + getAllFileNodeIds, + getChatInputNodeId, + getFileNodeId, + getNonFileTypeTweaks, + hasChatInputFiles, + hasFileTweaks, +} from "./detect-file-tweaks"; +/** Generates Python code using requests for API calls, handling multi-step file uploads (v1 for ChatInput, v2 for others) before flow execution. Supports auth. */ export function getNewPythonApiCode({ flowId, endpointName, processedPayload, + shouldDisplayApiKey, }: { flowId: string; endpointName: string; processedPayload: any; + shouldDisplayApiKey: boolean; }): string { const { protocol, host } = customGetHostProtocol(); - const apiUrl = `${protocol}//${host}/api/v1/run/${endpointName || flowId}`; + const baseUrl = `${protocol}//${host}`; - const payloadString = JSON.stringify(processedPayload, null, 4) - .replace(/true/g, "True") - .replace(/false/g, "False") - .replace(/null/g, "None"); + // Check if there are file uploads + const tweaks = processedPayload.tweaks || {}; + const hasFiles = hasFileTweaks(tweaks); - return `import requests + // If no file uploads, use existing logic + if (!hasFiles) { + const apiUrl = `${baseUrl}/api/v1/run/${endpointName || flowId}`; + const payloadString = JSON.stringify(processedPayload, null, 4) + .replace(/true/g, "True") + .replace(/false/g, "False") + .replace(/null/g, "None"); + + const authSection = shouldDisplayApiKey + ? `api_key = 'YOUR_API_KEY_HERE'` + : ""; + + const headersSection = shouldDisplayApiKey + ? `headers = {"x-api-key": api_key}` + : ""; + + return `import requests import os +import uuid -# API Configuration -try: - api_key = os.environ["LANGFLOW_API_KEY"] -except KeyError: - raise ValueError("LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables.") - -url = "${apiUrl}" # The complete API endpoint URL for this flow +${authSection}url = "${apiUrl}" # The complete API endpoint URL for this flow # Request payload configuration payload = ${payloadString} +payload["session_id"] = str(uuid.uuid4()) -# Request headers -headers = { - "Content-Type": "application/json",\n "x-api-key": api_key # Authentication key from environment variable -} +${headersSection} try: # Send API request @@ -48,4 +68,121 @@ except requests.exceptions.RequestException as e: print(f"Error making API request: {e}") except ValueError as e: print(f"Error parsing response: {e}")`; + } + + // File upload logic - handle multiple file types additively + const chatInputNodeIds = getAllChatInputNodeIds(tweaks); + const fileNodeIds = getAllFileNodeIds(tweaks); + const nonFileTweaks = getNonFileTypeTweaks(tweaks); + + if (chatInputNodeIds.length === 0 && fileNodeIds.length === 0) { + return getNewPythonApiCode({ + flowId, + endpointName, + processedPayload: { ...processedPayload, tweaks: nonFileTweaks }, + shouldDisplayApiKey, + }); + } + + const authSection = shouldDisplayApiKey + ? `api_key = 'YOUR_API_KEY_HERE'` + : ""; + + const headersSection = shouldDisplayApiKey + ? `headers = {"x-api-key": api_key}` + : ""; + + // Build upload steps for each file component + const uploadSteps: string[] = []; + const tweakAssignments: string[] = []; + + // ChatInput files (v1 API) + chatInputNodeIds.forEach((nodeId, index) => { + uploadSteps.push( + `# Step ${ + uploadSteps.length + 1 + }: Upload file for ChatInput ${nodeId}\nwith open(\"your_image_${ + index + 1 + }.jpg\", \"rb\") as f:\n response = requests.post(\n f\"{base_url}/api/v1/files/upload/{flow_id}\",\n headers=headers,\n files={\"file\": f}\n )\n response.raise_for_status()\n chat_file_path_${ + index + 1 + } = response.json()[\"file_path\"]`, + ); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + modifiedTweak.files = [`chat_file_path_${index + 1}`]; + tweakAssignments.push( + ` \"${nodeId}\": ${JSON.stringify(modifiedTweak, null, 4) + .split("\n") + .join("\n ")}`, + ); + }); + + // File/VideoFile components (v2 API) + fileNodeIds.forEach((nodeId, index) => { + uploadSteps.push( + `# Step ${ + uploadSteps.length + 1 + }: Upload file for File/VideoFile ${nodeId}\nwith open(\"your_file_${ + index + 1 + }.pdf\", \"rb\") as f:\n response = requests.post(\n f\"{base_url}/api/v2/files\",\n headers=headers,\n files={\"file\": f}\n )\n response.raise_for_status()\n file_path_${ + index + 1 + } = response.json()[\"path\"]`, + ); + + const originalTweak = tweaks[nodeId]; + const modifiedTweak = { ...originalTweak }; + if ("path" in originalTweak) { + modifiedTweak.path = [`file_path_${index + 1}`]; + } else if ("file_path" in originalTweak) { + modifiedTweak.file_path = `file_path_${index + 1}`; + } + tweakAssignments.push( + ` \"${nodeId}\": ${JSON.stringify(modifiedTweak, null, 4) + .split("\n") + .join("\n ")}`, + ); + }); + + // Add non-file tweaks + Object.entries(nonFileTweaks).forEach(([nodeId, tweak]) => { + tweakAssignments.push( + ` \"${nodeId}\": ${JSON.stringify(tweak, null, 4) + .split("\n") + .join("\n ")}`, + ); + }); + + const allTweaks = + tweakAssignments.length > 0 ? tweakAssignments.join(",\n") : ""; + + return `import requests +import os +import uuid + +${authSection}base_url = "${baseUrl}" +flow_id = "${flowId}" + +${headersSection} + +${uploadSteps.join("\n\n")} + +# Step ${uploadSteps.length + 1}: Execute flow with all file paths +payload = { + "output_type": "${processedPayload.output_type || "chat"}", + "input_type": "${processedPayload.input_type || "chat"}", + "input_value": "${processedPayload.input_value || "Your message here"}", + "session_id": str(uuid.uuid4()), + "tweaks": { +${allTweaks} + } +} + +response = requests.post( + f"{base_url}/api/v1/run/{endpointName or flowId}", + headers={"Content-Type": "application/json", **headers}, + json=payload +) +response.raise_for_status() +print(response.json())`; }