From 48ffdbf760aea027897e2e0a8d98c7b0c3e3c4e9 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 7 Aug 2024 12:26:24 -0300 Subject: [PATCH] feat: Add suggestion message to API exception response (#3149) * feat: suggest updating outdated components in API exception handling Suggest updating the outdated components in the API exception handling. This commit adds a suggestion message to the API exception response when there are outdated components in the flow. The suggestion message provides the number of outdated components and recommends updating them. The suggestion is generated based on the list of outdated components obtained from the flow data. * feat: refactor code * [autofix.ci] apply automated fixes * Update src/backend/base/langflow/exceptions/api.py Co-authored-by: Gabriel Luiz Freitas Almeida * Update src/backend/base/langflow/api/utils.py Co-authored-by: Gabriel Luiz Freitas Almeida * Update src/backend/base/langflow/exceptions/api.py Co-authored-by: Gabriel Luiz Freitas Almeida * update function name * [autofix.ci] apply automated fixes * refactor: fix import casing in langflow.api.utils and langflow.exceptions.api Co-authored-by: Gabriel Luiz Freitas Almeida * refactor: remove unused code and update exception handling in langflow.api.utils and langflow.exceptions.api * [autofix.ci] apply automated fixes * refactor: update exception handling and class in langflow.api.utils and langflow.exceptions.api * [autofix.ci] apply automated fixes * update function name and refactor none flow logic * [autofix.ci] apply automated fixes * refactor: fix typo in get_suggestion_message function name * refactor: improve get_suggestion_message function in langflow.api.utils * refactor: add unit tests for get_suggestion_message and get_outdated_components functions * refactor: add unit tests for APIException in langflow.exceptions.api * refactor: improve test coverage for APIException and related functions * [autofix.ci] apply automated fixes * update file name * refactor: update build_exception_body method in APIException to handle Exception type * refactor: handle None flow data in get_components_versions * [autofix.ci] apply automated fixes * refactor: update useDeleteBuilds function signature in _builds API query * fix: Fix test changing screen before request ended --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida --- src/backend/base/langflow/api/utils.py | 12 ++++ src/backend/base/langflow/api/v1/endpoints.py | 7 ++- src/backend/base/langflow/exceptions/api.py | 32 ++++++++++ .../services/database/models/flow/utils.py | 24 ++++++++ src/backend/tests/unit/api/test_api_utils.py | 41 +++++++++++++ src/backend/tests/unit/exceptions/test_api.py | 58 +++++++++++++++++++ .../tests/end-to-end/deleteFlows.spec.ts | 1 + 7 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/backend/tests/unit/api/test_api_utils.py create mode 100644 src/backend/tests/unit/exceptions/test_api.py 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();