diff --git a/src/backend/base/langflow/api/utils.py b/src/backend/base/langflow/api/utils.py index b16ab1324..d5d3e5b5a 100644 --- a/src/backend/base/langflow/api/utils.py +++ b/src/backend/base/langflow/api/utils.py @@ -215,3 +215,15 @@ def parse_exception(exc): if hasattr(exc, "body"): return exc.body["message"] return str(exc) + + +def get_suggestion_message(outdated_components: list[str]) -> str: + """Get the suggestion message for the outdated components.""" + count = len(outdated_components) + if count == 0: + return "The flow contains no outdated components." + elif count == 1: + return f"The flow contains 1 outdated component. We recommend updating the following component: {outdated_components[0]}." + else: + components = ", ".join(outdated_components) + return f"The flow contains {count} outdated components. We recommend updating the following components: {components}." diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index 296bfbba2..304de79f3 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -23,7 +23,7 @@ from langflow.api.v1.schemas import ( ) from langflow.custom.custom_component.component import Component from langflow.custom.utils import build_custom_component_template, get_instance_name -from langflow.exceptions.api import InvalidChatInputException +from langflow.exceptions.api import APIException, InvalidChatInputException from langflow.graph.graph.base import Graph from langflow.graph.schema import RunOutputs from langflow.helpers.flow import get_flow_by_id_or_endpoint_name @@ -260,7 +260,7 @@ async def simplified_run_flow( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc else: logger.exception(exc) - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc + raise APIException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=exc, flow=flow) from exc except InvalidChatInputException as exc: logger.error(exc) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc @@ -275,7 +275,8 @@ async def simplified_run_flow( runErrorMessage=str(exc), ), ) - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc + logger.exception(exc) + raise APIException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=exc, flow=flow) from exc @router.post("/webhook/{flow_id_or_name}", response_model=dict, status_code=HTTPStatus.ACCEPTED) diff --git a/src/backend/base/langflow/exceptions/api.py b/src/backend/base/langflow/exceptions/api.py index 8e9121733..70003b89b 100644 --- a/src/backend/base/langflow/exceptions/api.py +++ b/src/backend/base/langflow/exceptions/api.py @@ -1,2 +1,34 @@ +from fastapi import HTTPException +from langflow.api.utils import get_suggestion_message +from langflow.services.database.models.flow.model import Flow +from langflow.services.database.models.flow.utils import get_outdated_components +from pydantic import BaseModel + + class InvalidChatInputException(Exception): pass + + +# create a pidantic documentation for this class +class ExceptionBody(BaseModel): + message: str | list[str] + traceback: str | list[str] | None = None + description: str | list[str] | None = None + code: str | None = None + suggestion: str | list[str] | None = None + + +class APIException(HTTPException): + def __init__(self, exception: Exception, flow: Flow | None = None, status_code: int = 500): + body = self.build_exception_body(exception, flow) + super().__init__(status_code=status_code, detail=body.model_dump_json()) + + @staticmethod + def build_exception_body(exc: str | list[str] | Exception, flow: Flow | None) -> ExceptionBody: + body = {"message": str(exc)} + if flow: + outdated_components = get_outdated_components(flow) + if outdated_components: + body["suggestion"] = get_suggestion_message(outdated_components) + excep = ExceptionBody(**body) + return excep diff --git a/src/backend/base/langflow/services/database/models/flow/utils.py b/src/backend/base/langflow/services/database/models/flow/utils.py index 0cb45ebd7..02c654a24 100644 --- a/src/backend/base/langflow/services/database/models/flow/utils.py +++ b/src/backend/base/langflow/services/database/models/flow/utils.py @@ -1,6 +1,7 @@ from typing import Optional from fastapi import Depends +from langflow.utils.version import get_version_info from sqlmodel import Session from sqlalchemy import delete @@ -43,3 +44,26 @@ def get_all_webhook_components_in_flow(flow_data: dict | None): if not flow_data: return [] return [node for node in flow_data.get("nodes", []) if "Webhook" in node.get("id")] + + +def get_components_versions(flow: Flow): + versions: dict[str, str] = {} + if flow.data is None: + return versions + nodes = flow.data.get("nodes", []) + for node in nodes: + data = node.get("data", {}) + data_node = data.get("node", {}) + if "lf_version" in data_node: + versions[node["id"]] = data_node["lf_version"] + return versions + + +def get_outdated_components(flow: Flow): + component_versions = get_components_versions(flow) + lf_version = get_version_info()["version"] + outdated_components = [] + for key, value in component_versions.items(): + if value != lf_version: + outdated_components.append(key) + return outdated_components diff --git a/src/backend/tests/unit/api/test_api_utils.py b/src/backend/tests/unit/api/test_api_utils.py new file mode 100644 index 000000000..b21bc25d2 --- /dev/null +++ b/src/backend/tests/unit/api/test_api_utils.py @@ -0,0 +1,41 @@ +from langflow.api.utils import get_suggestion_message +from unittest.mock import patch +from langflow.services.database.models.flow.utils import get_outdated_components +from langflow.utils.version import get_version_info + + +def test_get_suggestion_message(): + # Test case 1: No outdated components + assert get_suggestion_message([]) == "The flow contains no outdated components." + + # Test case 2: One outdated component + assert ( + get_suggestion_message(["component1"]) + == "The flow contains 1 outdated component. We recommend updating the following component: component1." + ) + + # Test case 3: Multiple outdated components + outdated_components = ["component1", "component2", "component3"] + expected_message = "The flow contains 3 outdated components. We recommend updating the following components: component1, component2, component3." + assert get_suggestion_message(outdated_components) == expected_message + + +def test_get_outdated_components(): + # Mock data + flow = "mock_flow" + version = get_version_info()["version"] + mock_component_versions = { + "component1": version, + "component2": version, + "component3": "2.0", + } + # Expected result + expected_outdated_components = ["component3"] + + with patch( + "langflow.services.database.models.flow.utils.get_components_versions", return_value=mock_component_versions + ): + # Call the function with the mock flow + result = get_outdated_components(flow) + # Assert the result is as expected + assert result == expected_outdated_components diff --git a/src/backend/tests/unit/exceptions/test_api.py b/src/backend/tests/unit/exceptions/test_api.py new file mode 100644 index 000000000..542986f59 --- /dev/null +++ b/src/backend/tests/unit/exceptions/test_api.py @@ -0,0 +1,58 @@ +from unittest.mock import patch, Mock +from langflow.services.database.models.flow.model import Flow + + +def test_api_exception(): + from langflow.exceptions.api import APIException, ExceptionBody + + mock_exception = Exception("Test exception") + mock_flow = Mock(spec=Flow) + mock_outdated_components = ["component1", "component2"] + mock_suggestion_message = "Update component1, component2" + mock_component_versions = { + "component1": "1.0", + "component2": "1.0", + } + # Expected result + + with patch( + "langflow.services.database.models.flow.utils.get_outdated_components", return_value=mock_outdated_components + ): + with patch("langflow.api.utils.get_suggestion_message", return_value=mock_suggestion_message): + with patch( + "langflow.services.database.models.flow.utils.get_components_versions", + return_value=mock_component_versions, + ): + # Create an APIException instance + api_exception = APIException(mock_exception, mock_flow) + + # Expected body + expected_body = ExceptionBody( + message="Test exception", + suggestion="The flow contains 2 outdated components. We recommend updating the following components: component1, component2.", + ) + + # Assert the status code + assert api_exception.status_code == 500 + + # Assert the detail + assert api_exception.detail == expected_body.model_dump_json() + + +def test_api_exception_no_flow(): + from langflow.exceptions.api import APIException, ExceptionBody + + # Mock data + mock_exception = Exception("Test exception") + + # Create an APIException instance without a flow + api_exception = APIException(mock_exception) + + # Expected body + expected_body = ExceptionBody(message="Test exception") + + # Assert the status code + assert api_exception.status_code == 500 + + # Assert the detail + assert api_exception.detail == expected_body.model_dump_json() diff --git a/src/frontend/tests/end-to-end/deleteFlows.spec.ts b/src/frontend/tests/end-to-end/deleteFlows.spec.ts index 7314a9275..566781e0c 100644 --- a/src/frontend/tests/end-to-end/deleteFlows.spec.ts +++ b/src/frontend/tests/end-to-end/deleteFlows.spec.ts @@ -38,6 +38,7 @@ test("should delete a flow", async ({ page }) => { await page.getByTestId("install-Website Content QA").click(); + await page.getByText("Flow Installed Successfully.").nth(0).click(); await page.waitForSelector("text=My Collection", { timeout: 30000 }); await page.getByText("My Collection").nth(0).click();