Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add suggestion message to API exception response #3149

Merged
merged 32 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
594eaf9
feat: suggest updating outdated components in API exception handling
anovazzi1 Jul 31, 2024
bd52891
feat: refactor code
anovazzi1 Aug 1, 2024
acd08ff
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 1, 2024
fe1d4ad
Update src/backend/base/langflow/exceptions/api.py
anovazzi1 Aug 1, 2024
74b00c5
Update src/backend/base/langflow/api/utils.py
anovazzi1 Aug 1, 2024
e6c547d
Update src/backend/base/langflow/exceptions/api.py
anovazzi1 Aug 1, 2024
3edbd39
update function name
anovazzi1 Aug 1, 2024
d2702bf
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 1, 2024
80d3c5a
Merge branch 'main' into alertApi
anovazzi1 Aug 1, 2024
b2f579c
refactor: fix import casing in langflow.api.utils and langflow.except…
anovazzi1 Aug 2, 2024
bdecb06
refactor: remove unused code and update exception handling in langflo…
anovazzi1 Aug 2, 2024
8653560
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 2, 2024
6930307
Merge branch 'main' into alertApi
anovazzi1 Aug 2, 2024
ee41d66
refactor: update exception handling and class in langflow.api.utils …
anovazzi1 Aug 5, 2024
24eca7e
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 5, 2024
e3cd507
update function name and refactor none flow logic
anovazzi1 Aug 5, 2024
bc2e267
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 5, 2024
cc25625
refactor: fix typo in get_suggestion_message function name
anovazzi1 Aug 5, 2024
4b03ff6
refactor: improve get_suggestion_message function in langflow.api.utils
anovazzi1 Aug 5, 2024
068ca14
refactor: add unit tests for get_suggestion_message and get_outdated_…
anovazzi1 Aug 5, 2024
3e1c1c3
refactor: add unit tests for APIException in langflow.exceptions.api
anovazzi1 Aug 5, 2024
b3be9f6
refactor: improve test coverage for APIException and related functions
anovazzi1 Aug 5, 2024
4b83058
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 5, 2024
3476418
Merge branch 'main' into alertApi
anovazzi1 Aug 5, 2024
5ad2929
update file name
anovazzi1 Aug 6, 2024
4d75424
refactor: update build_exception_body method in APIException to handl…
anovazzi1 Aug 6, 2024
59596d3
refactor: handle None flow data in get_components_versions
anovazzi1 Aug 6, 2024
7001f9e
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 6, 2024
b58400d
refactor: update useDeleteBuilds function signature in _builds API query
anovazzi1 Aug 6, 2024
e5218a1
Merge branch 'main' into alertApi
anovazzi1 Aug 7, 2024
958121d
Merge branch 'main' into alertApi
anovazzi1 Aug 7, 2024
0da7c9f
fix: Fix test changing screen before request ended
anovazzi1 Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/backend/base/langflow/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}."
7 changes: 4 additions & 3 deletions src/backend/base/langflow/api/v1/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)
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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions src/backend/base/langflow/exceptions/api.py
Original file line number Diff line number Diff line change
@@ -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):
anovazzi1 marked this conversation as resolved.
Show resolved Hide resolved
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

anovazzi1 marked this conversation as resolved.
Show resolved Hide resolved

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
24 changes: 24 additions & 0 deletions src/backend/base/langflow/services/database/models/flow/utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
41 changes: 41 additions & 0 deletions src/backend/tests/unit/api/test_api_utils.py
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions src/backend/tests/unit/exceptions/test_api.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions src/frontend/tests/end-to-end/deleteFlows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down