diff --git a/src/backend/base/langflow/graph/utils.py b/src/backend/base/langflow/graph/utils.py index a5987df09..9ca656a3a 100644 --- a/src/backend/base/langflow/graph/utils.py +++ b/src/backend/base/langflow/graph/utils.py @@ -185,3 +185,19 @@ def log_vertex_build( logger.debug(f"Logged vertex build: {inserted.build_id}") except Exception as e: logger.exception(f"Error logging vertex build: {e}") + + +def rewrite_file_path(file_path: str): + file_path = file_path.replace("\\", "/") + + if ":" in file_path: + file_path = file_path.split(":", 1)[-1] + + file_path_split = [part for part in file_path.split("/") if part] + + if len(file_path_split) >= 2: + consistent_file_path = f"{file_path_split[-2]}/{file_path_split[-1]}" + else: + consistent_file_path = "/".join(file_path_split) + + return [consistent_file_path] diff --git a/src/backend/base/langflow/graph/vertex/types.py b/src/backend/base/langflow/graph/vertex/types.py index a44c4ef12..9a83e2ffe 100644 --- a/src/backend/base/langflow/graph/vertex/types.py +++ b/src/backend/base/langflow/graph/vertex/types.py @@ -8,7 +8,7 @@ from langchain_core.messages import AIMessage, AIMessageChunk from loguru import logger from langflow.graph.schema import CHAT_COMPONENTS, RECORDS_COMPONENTS, InterfaceComponentTypes, ResultData -from langflow.graph.utils import UnbuiltObject, log_transaction, log_vertex_build, serialize_field +from langflow.graph.utils import UnbuiltObject, log_transaction, log_vertex_build, rewrite_file_path, serialize_field from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.exceptions import NoComponentInstance from langflow.graph.vertex.schema import NodeData @@ -256,6 +256,10 @@ class InterfaceVertex(ComponentVertex): sender = self.params.get("sender", None) sender_name = self.params.get("sender_name", None) message = self.params.get(INPUT_FIELD_NAME, None) + files = self.params.get("files", []) + treat_file_path = files is not None and not isinstance(files, list) and isinstance(files, str) + if treat_file_path: + self.params["files"] = rewrite_file_path(files) files = [{"path": file} if isinstance(file, str) else file for file in self.params.get("files", [])] if isinstance(message, str): message = unescape_string(message) @@ -382,6 +386,12 @@ class InterfaceVertex(ComponentVertex): yield message complete_message += message + files = self.params.get("files", []) + + treat_file_path = files is not None and not isinstance(files, list) and isinstance(files, str) + if treat_file_path: + self.params["files"] = rewrite_file_path(files) + if hasattr(self.params.get("sender_name"), "get_text"): sender_name = self.params.get("sender_name").get_text() else: diff --git a/src/backend/base/langflow/services/database/models/message/model.py b/src/backend/base/langflow/services/database/models/message/model.py index 69e7458c1..79d547606 100644 --- a/src/backend/base/langflow/services/database/models/message/model.py +++ b/src/backend/base/langflow/services/database/models/message/model.py @@ -30,6 +30,13 @@ class MessageBase(SQLModel): # first check if the record has all the required fields if message.text is None or not message.sender or not message.sender_name: raise ValueError("The message does not have the required fields (text, sender, sender_name).") + if message.files: + image_paths = [] + for file in message.files: + if hasattr(file, "path") and hasattr(file, "url") and file.path: + session_id = message.session_id + image_paths.append(f"{session_id}{file.path.split(session_id)[1]}") + message.files = image_paths if isinstance(message.timestamp, str): timestamp = datetime.fromisoformat(message.timestamp) else: diff --git a/src/backend/tests/unit/utils/test_rewrite_file_path.py b/src/backend/tests/unit/utils/test_rewrite_file_path.py new file mode 100644 index 000000000..f287025b5 --- /dev/null +++ b/src/backend/tests/unit/utils/test_rewrite_file_path.py @@ -0,0 +1,43 @@ +from langflow.graph.utils import rewrite_file_path +import pytest + + +@pytest.mark.parametrize( + "file_path, expected", + [ + # Test case 1: Standard path with multiple directories + ("/home/user/documents/file.txt", ["documents/file.txt"]), + # Test case 2: Path with only one directory + ("/documents/file.txt", ["documents/file.txt"]), + # Test case 3: Path with no directories (just filename) + ("file.txt", ["file.txt"]), + # Test case 4: Path with multiple levels and special characters + ("/home/user/my-docs/special_file!.pdf", ["my-docs/special_file!.pdf"]), + # Test case 5: Path with trailing slash + ("/home/user/documents/", ["user/documents"]), + # Test case 6: Empty path + ("", [""]), + # Test case 7: Path with only slashes + ("///", [""]), + # Test case 8: Path with dots + ("/home/user/../documents/./file.txt", ["./file.txt"]), + # Test case 9: Windows-style path + ("C:\\Users\\Documents\\file.txt", ["Documents/file.txt"]), + # Test case 10: Windows path with trailing backslash + ("C:\\Users\\Documents\\", ["Users/Documents"]), + # Test case 11: Mixed separators + ("C:/Users\\Documents/file.txt", ["Documents/file.txt"]), + # Test case 12: Network path (UNC) + ("\\\\server\\share\\file.txt", ["share/file.txt"]), + ], +) +def test_rewrite_file_path(file_path, expected): + result = rewrite_file_path(file_path) + assert result == expected + + +# Additional test for type checking +def test_rewrite_file_path_type(): + result = rewrite_file_path("/home/user/file.txt") + assert isinstance(result, list) + assert all(isinstance(item, str) for item in result) diff --git a/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts b/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts index befb054a6..d60e794c5 100644 --- a/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts +++ b/src/frontend/tests/core/features/chatInputOutputUser-shard-0.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { test } from "@playwright/test"; import * as dotenv from "dotenv"; import { readFileSync } from "fs"; import path from "path"; diff --git a/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts b/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts new file mode 100644 index 000000000..5d8968ad5 --- /dev/null +++ b/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts @@ -0,0 +1,114 @@ +import { expect, test } from "@playwright/test"; +import * as dotenv from "dotenv"; +import path from "path"; + +test("user must be able to send an image on chat using advanced tool on ChatInputComponent", async ({ + page, +}) => { + test.skip( + !process?.env?.OPENAI_API_KEY, + "OPENAI_API_KEY required to run this test", + ); + + if (!process.env.CI) { + dotenv.config({ path: path.resolve(__dirname, "../../.env") }); + } + + await page.goto("/"); + + await page.waitForTimeout(1000); + + let modalCount = 0; + try { + const modalTitleElement = await page?.getByTestId("modal-title"); + if (modalTitleElement) { + modalCount = await modalTitleElement.count(); + } + } catch (error) { + modalCount = 0; + } + + while (modalCount === 0) { + await page.getByText("New Project", { exact: true }).click(); + await page.waitForTimeout(3000); + modalCount = await page.getByTestId("modal-title")?.count(); + } + + await page.getByRole("heading", { name: "Basic Prompting" }).click(); + await page.waitForSelector('[title="fit view"]', { + timeout: 100000, + }); + + await page.getByTitle("fit view").click(); + await page.getByTitle("zoom out").click(); + await page.getByTitle("zoom out").click(); + await page.getByTitle("zoom out").click(); + + let outdatedComponents = await page.getByTestId("icon-AlertTriangle").count(); + + while (outdatedComponents > 0) { + await page.getByTestId("icon-AlertTriangle").first().click(); + await page.waitForTimeout(1000); + outdatedComponents = await page.getByTestId("icon-AlertTriangle").count(); + } + + await page + .getByTestId("popover-anchor-input-api_key") + .fill(process.env.OPENAI_API_KEY ?? ""); + + await page.getByTestId("dropdown_str_model_name").click(); + await page.getByTestId("gpt-4o-1-option").click(); + + await page.waitForSelector("text=Chat Input", { timeout: 30000 }); + + await page.getByText("Chat Input", { exact: true }).click(); + await page.getByTestId("more-options-modal").click(); + await page.getByTestId("edit-button-modal").click(); + await page.getByTestId("showfiles").click(); + await page.getByText("Close").last().click(); + + await page.waitForTimeout(500); + + const userQuestion = "What is this image?"; + await page.getByTestId("textarea_str_input_value").fill(userQuestion); + + const filePath = "tests/assets/chain.png"; + + await page.click('[data-testid="inputfile_file_files"]'); + + const [fileChooser] = await Promise.all([ + page.waitForEvent("filechooser"), + page.click('[data-testid="inputfile_file_files"]'), + ]); + + await fileChooser.setFiles(filePath); + + await page.keyboard.press("Escape"); + + await page.getByTestId("button_run_chat output").click(); + await page.getByText("built successfully").last().click({ + timeout: 15000, + }); + + await page.getByText("Playground", { exact: true }).click(); + + await page.waitForTimeout(500); + + await page.waitForSelector('[data-testid="icon-LucideSend"]', { + timeout: 100000, + }); + + await page.waitForSelector("text=chain.png", { timeout: 30000 }); + + expect(await page.getByAltText("generated image").isVisible()).toBeTruthy(); + + expect( + await page.getByTestId(`chat-message-User-${userQuestion}`).isVisible(), + ).toBeTruthy(); + + const textContents = await page + .getByTestId("div-chat-message") + .allTextContents(); + + expect(textContents[0]).toContain("chain"); +});