diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index dedd30478..018c5cdaa 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -30,7 +30,7 @@ from langflow.schema.graph import Tweaks from langflow.services.auth.utils import api_key_security, get_current_active_user from langflow.services.cache.utils import save_uploaded_file from langflow.services.database.models.flow import Flow -from langflow.services.database.models.flow.utils import get_all_webhook_components_in_flow, get_flow_by_id +from langflow.services.database.models.flow.utils import get_all_webhook_components_in_flow from langflow.services.database.models.user.model import User from langflow.services.deps import ( get_cache_service, @@ -211,10 +211,10 @@ async def simplified_run_flow( raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc -@router.post("/webhook/{flow_id}", response_model=dict, status_code=HTTPStatus.ACCEPTED) +@router.post("/webhook/{flow_id_or_name}", response_model=dict, status_code=HTTPStatus.ACCEPTED) async def webhook_run_flow( db: Annotated[Session, Depends(get_session)], - flow: Annotated[Flow, Depends(get_flow_by_id)], + flow: Annotated[Flow, Depends(get_flow_by_id_or_endpoint_name)], request: Request, background_tasks: BackgroundTasks, session_service: SessionService = Depends(get_session_service), diff --git a/tests/conftest.py b/tests/conftest.py index 780a2c7e1..b3f537a3c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,9 +14,6 @@ from dotenv import load_dotenv from fastapi.testclient import TestClient from httpx import AsyncClient from sqlmodel import Session, SQLModel, create_engine, select -from sqlmodel.pool import StaticPool -from typer.testing import CliRunner - from langflow.graph.graph.base import Graph from langflow.initial_setup.setup import STARTER_FOLDER_NAME from langflow.services.auth.utils import get_password_hash @@ -26,6 +23,9 @@ from langflow.services.database.models.folder.model import Folder from langflow.services.database.models.user.model import User, UserCreate from langflow.services.database.utils import session_getter from langflow.services.deps import get_db_service +from sqlmodel import Session, SQLModel, create_engine, select +from sqlmodel.pool import StaticPool +from typer.testing import CliRunner if TYPE_CHECKING: from langflow.services.database.service import DatabaseService @@ -395,7 +395,9 @@ def added_vector_store(client, json_vector_store, logged_in_headers): def added_webhook_test(client, json_webhook_test, logged_in_headers): webhook_test = orjson.loads(json_webhook_test) data = webhook_test["data"] - webhook_test = FlowCreate(name="Webhook Test", description="description", data=data) + webhook_test = FlowCreate( + name="Webhook Test", description="description", data=data, endpoint_name=webhook_test["endpoint_name"] + ) response = client.post("api/v1/flows/", json=webhook_test.model_dump(), headers=logged_in_headers) assert response.status_code == 201 assert response.json()["name"] == webhook_test.name diff --git a/tests/data/WebhookTest.json b/tests/data/WebhookTest.json index 71a7141f4..49c33d100 100644 --- a/tests/data/WebhookTest.json +++ b/tests/data/WebhookTest.json @@ -1 +1,238 @@ -{"id":"40b2ae66-384b-4978-85ab-f79706287a1a","data":{"nodes":[{"id":"Webhook-8rYwr","type":"genericNode","position":{"x":375.90734417043643,"y":261.63442970852185},"data":{"type":"Webhook","node":{"template":{"_type":"CustomComponent","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"import json\nimport uuid\nfrom typing import Any, Optional\n\nfrom langflow.custom import CustomComponent\nfrom langflow.schema import Data\nfrom langflow.schema.dotdict import dotdict\n\n\nclass WebhookComponent(CustomComponent):\n display_name = \"Webhook Input\"\n description = \"Defines a webhook input for the flow.\"\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):\n if field_name == \"webhook_id\":\n build_config[\"webhook_id\"][\"value\"] = uuid.uuid4().hex\n return build_config\n\n def build_config(self):\n return {\n \"data\": {\n \"display_name\": \"Data\",\n \"info\": \"Use this field to quickly test the webhook component by providing a JSON payload.\",\n \"multiline\": True,\n }\n }\n\n def build(self, data: Optional[str] = \"\") -> Data:\n message = \"\"\n try:\n body = json.loads(data or \"{}\")\n except json.JSONDecodeError:\n body = {\"payload\": data}\n message = f\"Invalid JSON payload. Please check the format.\\n\\n{data}\"\n record = Data(data=body)\n if not message:\n message = record\n self.status = message\n return record\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"data":{"type":"str","required":false,"placeholder":"","list":false,"show":true,"multiline":true,"value":"{\"bacon\":1}","fileTypes":[],"file_path":"","password":false,"name":"data","display_name":"Data","advanced":false,"dynamic":false,"info":"Use this field to quickly test the webhook component by providing a JSON payload.","load_from_db":false,"title_case":false,"input_types":["Text"]}},"description":"Defines a webhook input for the flow.","base_classes":["Data"],"display_name":"Webhook Input","documentation":"","custom_fields":{"data":null},"output_types":["Data"],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Data"],"selected":"Data","name":"data","hidden":false,"display_name":"Data","method":null,"value":"__UNDEFINED__","cache":true}],"field_order":[],"beta":false,"edited":true},"id":"Webhook-8rYwr","description":"Defines a webhook input for the flow.","display_name":"Webhook Input","edited":false},"selected":false,"width":384,"height":309,"dragging":false,"positionAbsolute":{"x":375.90734417043643,"y":261.63442970852185}},{"id":"CustomComponent-xbSW2","type":"genericNode","position":{"x":888.0012384532345,"y":274.9520639008431},"data":{"type":"CustomComponent","node":{"template":{"_type":"Component","code":{"type":"code","required":true,"placeholder":"","list":false,"show":true,"multiline":true,"value":"# from langflow.field_typing import Data\nfrom langflow.custom import Component\nfrom langflow.inputs import StrInput\nfrom langflow.schema import Data\nfrom langflow.template import Output\n\n\nclass CustomComponent(Component):\n display_name = \"Custom Component\"\n description = \"Use as a template to create your own component.\"\n documentation: str = \"http://docs.langflow.org/components/custom\"\n icon = \"custom_components\"\n\n inputs = [\n StrInput(name=\"input_value\", display_name=\"Input Value\", value=\"Hello, World!\", input_types=[\"Data\"]),\n ]\n\n outputs = [\n Output(display_name=\"Output\", name=\"output\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n if isinstance(self.input_value, Data):\n data = self.input_value\n else:\n data = Data(value=self.input_value)\n \n if \"path\" in data:\n path = self.resolve_path(data.path)\n path_obj = Path(path)\n with open(path, \"w\") as f:\n f.write(data.model_dump())\n self.status = data\n return data\n","fileTypes":[],"file_path":"","password":false,"name":"code","advanced":true,"dynamic":true,"info":"","load_from_db":false,"title_case":false},"input_value":{"load_from_db":false,"list":false,"required":false,"placeholder":"","show":true,"value":"","name":"input_value","display_name":"Input Value","advanced":false,"input_types":["Data"],"dynamic":false,"info":"","title_case":false,"type":"str"}},"description":"Use as a template to create your own component.","icon":"custom_components","base_classes":["Data"],"display_name":"Custom Component","documentation":"http://docs.langflow.org/components/custom","custom_fields":{},"output_types":[],"pinned":false,"conditional_paths":[],"frozen":false,"outputs":[{"types":["Data"],"selected":"Data","name":"output","display_name":"Output","method":"build_output","value":"__UNDEFINED__","cache":true}],"field_order":["input_value"],"beta":false,"edited":true},"id":"CustomComponent-xbSW2","description":"Use as a template to create your own component.","display_name":"Custom Component"},"selected":false,"width":384,"height":337,"positionAbsolute":{"x":888.0012384532345,"y":274.9520639008431},"dragging":false}],"edges":[{"source":"Webhook-8rYwr","sourceHandle":"{œdataTypeœ:œWebhookœ,œidœ:œWebhook-8rYwrœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}","target":"CustomComponent-xbSW2","targetHandle":"{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-xbSW2œ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}","data":{"targetHandle":{"fieldName":"input_value","id":"CustomComponent-xbSW2","inputTypes":["Data"],"type":"str"},"sourceHandle":{"dataType":"Webhook","id":"Webhook-8rYwr","name":"data","output_types":["Data"]}},"id":"reactflow__edge-Webhook-8rYwr{œdataTypeœ:œWebhookœ,œidœ:œWebhook-8rYwrœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}-CustomComponent-xbSW2{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-xbSW2œ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}"}],"viewport":{"x":-150.0437656191234,"y":128.9195326354088,"zoom":0.7432914922155076}},"description":"The Power of Language at Your Fingertips.","name":"Webhook Test","last_tested_version":"1.0.0a52","is_component":false} \ No newline at end of file +{ + "id": "40b2ae66-384b-4978-85ab-f79706287a1a", + "data": { + "nodes": [ + { + "id": "Webhook-8rYwr", + "type": "genericNode", + "position": { + "x": 375.90734417043643, + "y": 261.63442970852185 + }, + "data": { + "type": "Webhook", + "node": { + "template": { + "_type": "CustomComponent", + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "import json\nimport uuid\nfrom typing import Any, Optional\n\nfrom langflow.custom import CustomComponent\nfrom langflow.schema import Data\nfrom langflow.schema.dotdict import dotdict\n\n\nclass WebhookComponent(CustomComponent):\n display_name = \"Webhook Input\"\n description = \"Defines a webhook input for the flow.\"\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):\n if field_name == \"webhook_id\":\n build_config[\"webhook_id\"][\"value\"] = uuid.uuid4().hex\n return build_config\n\n def build_config(self):\n return {\n \"data\": {\n \"display_name\": \"Data\",\n \"info\": \"Use this field to quickly test the webhook component by providing a JSON payload.\",\n \"multiline\": True,\n }\n }\n\n def build(self, data: Optional[str] = \"\") -> Data:\n message = \"\"\n try:\n body = json.loads(data or \"{}\")\n except json.JSONDecodeError:\n body = {\"payload\": data}\n message = f\"Invalid JSON payload. Please check the format.\\n\\n{data}\"\n record = Data(data=body)\n if not message:\n message = record\n self.status = message\n return record\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "load_from_db": false, + "title_case": false + }, + "data": { + "type": "str", + "required": false, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "{\"bacon\":1}", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "data", + "display_name": "Data", + "advanced": false, + "dynamic": false, + "info": "Use this field to quickly test the webhook component by providing a JSON payload.", + "load_from_db": false, + "title_case": false, + "input_types": [ + "Text" + ] + } + }, + "description": "Defines a webhook input for the flow.", + "base_classes": [ + "Data" + ], + "display_name": "Webhook Input", + "documentation": "", + "custom_fields": { + "data": null + }, + "output_types": [ + "Data" + ], + "pinned": false, + "conditional_paths": [], + "frozen": false, + "outputs": [ + { + "types": [ + "Data" + ], + "selected": "Data", + "name": "data", + "hidden": false, + "display_name": "Data", + "method": null, + "value": "__UNDEFINED__", + "cache": true + } + ], + "field_order": [], + "beta": false, + "edited": true + }, + "id": "Webhook-8rYwr", + "description": "Defines a webhook input for the flow.", + "display_name": "Webhook Input", + "edited": false + }, + "selected": false, + "width": 384, + "height": 309, + "dragging": false, + "positionAbsolute": { + "x": 375.90734417043643, + "y": 261.63442970852185 + } + }, + { + "id": "CustomComponent-xbSW2", + "type": "genericNode", + "position": { + "x": 888.0012384532345, + "y": 274.9520639008431 + }, + "data": { + "type": "CustomComponent", + "node": { + "template": { + "_type": "Component", + "code": { + "type": "code", + "required": true, + "placeholder": "", + "list": false, + "show": true, + "multiline": true, + "value": "# from langflow.field_typing import Data\nfrom langflow.custom import Component\nfrom langflow.inputs import StrInput\nfrom langflow.schema import Data\nfrom langflow.template import Output\n\n\nclass CustomComponent(Component):\n display_name = \"Custom Component\"\n description = \"Use as a template to create your own component.\"\n documentation: str = \"http://docs.langflow.org/components/custom\"\n icon = \"custom_components\"\n\n inputs = [\n StrInput(name=\"input_value\", display_name=\"Input Value\", value=\"Hello, World!\", input_types=[\"Data\"]),\n ]\n\n outputs = [\n Output(display_name=\"Output\", name=\"output\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n if isinstance(self.input_value, Data):\n data = self.input_value\n else:\n data = Data(value=self.input_value)\n \n if \"path\" in data:\n path = self.resolve_path(data.path)\n path_obj = Path(path)\n with open(path, \"w\") as f:\n f.write(data.model_dump_json())\n self.status = data\n return data\n", + "fileTypes": [], + "file_path": "", + "password": false, + "name": "code", + "advanced": true, + "dynamic": true, + "info": "", + "load_from_db": false, + "title_case": false + }, + "input_value": { + "load_from_db": false, + "list": false, + "required": false, + "placeholder": "", + "show": true, + "value": "", + "name": "input_value", + "display_name": "Input Value", + "advanced": false, + "input_types": [ + "Data" + ], + "dynamic": false, + "info": "", + "title_case": false, + "type": "str" + } + }, + "description": "Use as a template to create your own component.", + "icon": "custom_components", + "base_classes": [ + "Data" + ], + "display_name": "Custom Component", + "documentation": "http://docs.langflow.org/components/custom", + "custom_fields": {}, + "output_types": [], + "pinned": false, + "conditional_paths": [], + "frozen": false, + "outputs": [ + { + "types": [ + "Data" + ], + "selected": "Data", + "name": "output", + "display_name": "Output", + "method": "build_output", + "value": "__UNDEFINED__", + "cache": true + } + ], + "field_order": [ + "input_value" + ], + "beta": false, + "edited": true + }, + "id": "CustomComponent-xbSW2", + "description": "Use as a template to create your own component.", + "display_name": "Custom Component" + }, + "selected": false, + "width": 384, + "height": 337, + "positionAbsolute": { + "x": 888.0012384532345, + "y": 274.9520639008431 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "Webhook-8rYwr", + "sourceHandle": "{œdataTypeœ:œWebhookœ,œidœ:œWebhook-8rYwrœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}", + "target": "CustomComponent-xbSW2", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-xbSW2œ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}", + "data": { + "targetHandle": { + "fieldName": "input_value", + "id": "CustomComponent-xbSW2", + "inputTypes": [ + "Data" + ], + "type": "str" + }, + "sourceHandle": { + "dataType": "Webhook", + "id": "Webhook-8rYwr", + "name": "data", + "output_types": [ + "Data" + ] + } + }, + "id": "reactflow__edge-Webhook-8rYwr{œdataTypeœ:œWebhookœ,œidœ:œWebhook-8rYwrœ,œnameœ:œdataœ,œoutput_typesœ:[œDataœ]}-CustomComponent-xbSW2{œfieldNameœ:œinput_valueœ,œidœ:œCustomComponent-xbSW2œ,œinputTypesœ:[œDataœ],œtypeœ:œstrœ}", + "className": "" + } + ], + "viewport": { + "x": 0, + "y": 0, + "zoom": 1 + } + }, + "description": "The Power of Language at Your Fingertips.", + "name": "Webhook Test", + "last_tested_version": "1.0.0a52", + "endpoint_name": "webhook-test", + "is_component": false +} \ No newline at end of file diff --git a/tests/test_webhook.py b/tests/test_webhook.py new file mode 100644 index 000000000..c8d75c9cc --- /dev/null +++ b/tests/test_webhook.py @@ -0,0 +1,28 @@ +import tempfile +from pathlib import Path + + +def test_webhook_endpoint(client, added_webhook_test): + # The test is as follows: + # 1. The flow when run will get a "path" from the payload and save a file with the path as the name. + # We will create a temporary file path and send it to the webhook endpoint, then check if the file exists. + # 2. we will delete the file, then send an invalid payload to the webhook endpoint and check if the file exists. + endpoint_name = added_webhook_test["endpoint_name"] + endpoint = f"api/v1/webhook/{endpoint_name}" + # Create a temporary file + with tempfile.TemporaryDirectory() as tmp: + file_path = Path(tmp) / "test_file.txt" + + payload = {"path": str(file_path)} + + response = client.post(endpoint, json=payload) + assert response.status_code == 202 + assert file_path.exists() + + assert not file_path.exists() + + # Send an invalid payload + payload = {"invalid_key": "invalid_value"} + response = client.post(endpoint, json=payload) + assert response.status_code == 202 + assert not file_path.exists()