From 1a5156784c13f599802e47326cd01bd75f62461e Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:26:22 -0300 Subject: [PATCH 01/14] fix: deploy buttons images not appearing (#3092) Added railway, render and GCP deploy images and link --- docs/docs/Deployment/deployment-gcp.md | 24 +--------------------- docs/docs/Deployment/deployment-railway.md | 11 ++-------- docs/docs/Deployment/deployment-render.md | 21 +++---------------- docs/docs/Deployment/railway-deploy.svg | 1 + docs/docs/Deployment/render-deploy.svg | 6 ++++++ 5 files changed, 13 insertions(+), 50 deletions(-) create mode 100644 docs/docs/Deployment/railway-deploy.svg create mode 100644 docs/docs/Deployment/render-deploy.svg diff --git a/docs/docs/Deployment/deployment-gcp.md b/docs/docs/Deployment/deployment-gcp.md index 913e03097e79..70644a429902 100644 --- a/docs/docs/Deployment/deployment-gcp.md +++ b/docs/docs/Deployment/deployment-gcp.md @@ -4,68 +4,46 @@ sidebar_position: 3 slug: /deployment-gcp --- - - :::info This page may contain outdated information. It will be updated as soon as possible. ::: - - - ## Deploy on Google Cloud Platform {#4ee01cda736c4f7396936409f23cdb52} - --- - ### Run Langflow from a New Google Cloud Project {#ce729796d7404ccdb627bee47d6a4399} - This guide will help you set up a Langflow development VM in a Google Cloud Platform project using Google Cloud Shell. - :::info When Cloud Shell opens, be sure to select Trust repo. Some gcloud commands might not run in an ephemeral Cloud Shell environment. ::: - - - ### Standard VM {#245b47b450dd4159a5c56a5124bab84f} - -[embed](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/langflow-ai/langflow&working_dir=scripts/gcp&shellonly=true&tutorial=walkthroughtutorial.md) - +[![GCP Deploy](https://camo.githubusercontent.com/c1a4a499c1d93d7038fd7af8c4f3fce222050f3bbdf4275dafbfde2491e4b8c4/68747470733a2f2f677374617469632e636f6d2f636c6f75647373682f696d616765732f6f70656e2d62746e2e737667)](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/langflow-ai/langflow&working_dir=scripts/gcp&shellonly=true&tutorial=walkthroughtutorial.md) This script sets up a Debian-based VM with the Langflow package, Nginx, and the necessary configurations to run the Langflow Dev environment. - --- - ## Spot/Preemptible Instance {#de9b8f7c71284cbb98e8137a3c44553d} - When running as a [spot (preemptible) instance](https://cloud.google.com/compute/docs/instances/preemptible), the code and VM will behave the same way as in a regular instance, executing the startup script to configure the environment, install necessary dependencies, and run the Langflow application. However, **due to the nature of spot instances, the VM may be terminated at any time if Google Cloud needs to reclaim the resources**. This makes spot instances suitable for fault-tolerant, stateless, or interruptible workloads that can handle unexpected terminations and restarts. - --- - ## Pricing (approximate) {#2289f4ba9f544e6e9d4b915ef5aacd24} - > For a more accurate breakdown of costs, please use the GCP Pricing Calculator - | Component | Regular Cost (Hourly) | Regular Cost (Monthly) | Spot/Preemptible Cost (Hourly) | Spot/Preemptible Cost (Monthly) | Notes | | ------------------ | --------------------- | ---------------------- | ------------------------------ | ------------------------------- | -------------------------------------------------------------------------- | | 100 GB Disk | - | $10/month | - | $10/month | Disk cost remains the same for both regular and Spot/Preemptible VMs | | VM (n1-standard-4) | $0.15/hr | ~$108/month | ~$0.04/hr | ~$29/month | The VM cost can be significantly reduced using a Spot/Preemptible instance | | **Total** | **$0.15/hr** | **~$118/month** | **~$0.04/hr** | **~$39/month** | Total costs for running the VM and disk 24/7 for an entire month | - diff --git a/docs/docs/Deployment/deployment-railway.md b/docs/docs/Deployment/deployment-railway.md index 6ab0a02b9437..cf148b3166d5 100644 --- a/docs/docs/Deployment/deployment-railway.md +++ b/docs/docs/Deployment/deployment-railway.md @@ -4,27 +4,20 @@ sidebar_position: 5 slug: /deployment-railway --- - - ## Deploy on Railway {#a9a1ce4d39e74cc29aef4d30c6172d10} - --- - Railway is a cloud infrastructure platform that enables developers to deploy and manage applications effortlessly. It provides an intuitive interface, seamless integrations, and powerful features like auto-deploy from GitHub, managed databases, and automatic scaling. - Deploying Langflow to Railway involves a few simple steps: 1. **Click the Button Below**: Start by clicking the deployment button provided below. This will redirect you to the Railway platform. - [https://railway.app/template/JMXEWp?referralCode=MnPSdg](https://railway.app/template/JMXEWp?referralCode=MnPSdg) + [![Deploy on Railway](./railway-deploy.svg)](https://railway.app/template/JMXEWp?referralCode=MnPSdg) 2. **Deploy**: Proceed to deploy your Langflow instance. Click Deploy Now to deploy the instance. Railway will handle the rest, including setting up the infrastructure, deploying the Langflow instance, and starting the application. - ![](./1098199232.png) - + ![](./1098199232.png) By following these steps, your Langflow instance will be successfully deployed on Railway. - diff --git a/docs/docs/Deployment/deployment-render.md b/docs/docs/Deployment/deployment-render.md index 974d763381a3..bde29267d0eb 100644 --- a/docs/docs/Deployment/deployment-render.md +++ b/docs/docs/Deployment/deployment-render.md @@ -4,39 +4,24 @@ sidebar_position: 4 slug: /deployment-render --- - - ## Deploy on Render {#20a959b7047e44e490cc129fd21895c0} - --- - [Render.com](http://render.com/) is a unified cloud platform designed to make deploying web applications, APIs, and static sites easy. It provides a streamlined experience with powerful features like automatic SSL, managed databases, and auto-deploy from Git, making it a popular choice for developers looking to simplify their deployment workflows. - Deploying Langflow to Render is a straightforward process that can be completed in just a few steps: - 1. **Click the Button Below**: Start by clicking the deployment button provided below. This will redirect you to the Render platform. - - [https://render.com/deploy?repo=https://github.com/langflow-ai/langflow/tree/dev](https://render.com/deploy?repo=https%3A%2F%2Fgithub.com%2Flangflow-ai%2Flangflow%2Ftree%2Fdev) - + [![Deploy to Render](./render-deploy.svg)](https://render.com/deploy?repo=https%3A%2F%2Fgithub.com%2Flangflow-ai%2Flangflow%2Ftree%2Fdev) 2. **Select the Default Configuration**: Once on the Render platform, you will be prompted to provide a blueprint name and to select the default configuration for Langflow. This configuration includes all the necessary settings and resources to run Langflow efficiently. You can change the branch of the repo to “main” or “dev” based on your preference. Click “Create New Resources” to proceed. - - - ![](./1861599636.png) - - + ![](./1861599636.png) 3. **Deploy**: After selecting the configuration, proceed to deploy your Langflow instance. You can keep the default Starter instance, or change it to another instance based on your specific needs. Click Apply to deploy the instance. Render will handle the rest, including setting up the database, deploying the Langflow instance, and starting the application. - - ![](./1929176153.png) - + ![](./1929176153.png) By following these steps, your Langflow instance will be successfully deployed on Render. Remember to review the pricing details on the Render platform to understand any costs involved. - diff --git a/docs/docs/Deployment/railway-deploy.svg b/docs/docs/Deployment/railway-deploy.svg new file mode 100644 index 000000000000..73df2c13e22e --- /dev/null +++ b/docs/docs/Deployment/railway-deploy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/docs/Deployment/render-deploy.svg b/docs/docs/Deployment/render-deploy.svg new file mode 100644 index 000000000000..7e137639f3ef --- /dev/null +++ b/docs/docs/Deployment/render-deploy.svg @@ -0,0 +1,6 @@ + + + + + + From aa46f320abdb8d31ca3c14c9c1f39fe2b22ed3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 31 Jul 2024 15:43:00 +0200 Subject: [PATCH 02/14] fix: starter projects refresh don't add new fields (#3110) export stremaing module --- .../base/langflow/initial_setup/setup.py | 1 + .../Basic Prompting (Hello, World).json | 49 +++++++++++ .../starter_projects/Blog Writer.json | 33 +++++++ .../starter_projects/Complex Agent.json | 85 +++++++++++++++++++ .../starter_projects/Document QA.json | 49 +++++++++++ .../starter_projects/Memory Chatbot.json | 49 +++++++++++ .../starter_projects/Sequential Agent.json | 17 ++++ src/backend/tests/unit/test_initial_setup.py | 4 + 8 files changed, 287 insertions(+) diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index dc38a7df9f57..c4a43d4cf453 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -102,6 +102,7 @@ def update_projects_components_with_latest_component_versions(project_data, all_ for field_name, field_dict in latest_template.items(): if field_name not in node_data["template"]: + node_data["template"][field_name] = field_dict continue # The idea here is to update some attributes of the field to_check_attributes = FIELD_FORMAT_ATTRIBUTES diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json index 9620ae795ed3..c41428891730 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json @@ -262,6 +262,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, @@ -559,6 +575,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, @@ -640,6 +672,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json index 7bdf340b25e9..a341db0f31c6 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json @@ -763,6 +763,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, @@ -844,6 +860,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json index 4f73d83af5b7..a6ab3af9cf68 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json @@ -902,6 +902,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, @@ -2010,6 +2027,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, @@ -2691,6 +2725,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, @@ -3092,6 +3143,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, @@ -3518,6 +3586,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json b/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json index cd87cb9cef52..94a094f9d356 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json @@ -459,6 +459,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, @@ -637,6 +653,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, @@ -718,6 +750,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json index c1e124d3a374..9641e99ab677 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json @@ -433,6 +433,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, @@ -514,6 +530,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, @@ -889,6 +922,22 @@ "trace_as_metadata": true, "type": "str", "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "name": "should_store_message", + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "type": "bool", + "value": true } } }, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json index 3ee5a505ef23..ee1986dac491 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json @@ -2048,6 +2048,23 @@ "pinned": false, "template": { "_type": "Component", + "api_key": { + "_input_type": "SecretStrInput", + "advanced": false, + "display_name": "OpenAI API Key", + "dynamic": false, + "info": "The OpenAI API Key to use for the OpenAI model.", + "input_types": [], + "load_from_db": true, + "name": "api_key", + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "type": "str", + "value": "OPENAI_API_KEY" + }, "code": { "advanced": true, "dynamic": true, diff --git a/src/backend/tests/unit/test_initial_setup.py b/src/backend/tests/unit/test_initial_setup.py index 1e67c764ee9e..395f3443b240 100644 --- a/src/backend/tests/unit/test_initial_setup.py +++ b/src/backend/tests/unit/test_initial_setup.py @@ -129,6 +129,7 @@ async def test_refresh_starter_projects(): chat_input = find_componeny_by_name(components, "ChatInput") chat_output = find_componeny_by_name(components, "ChatOutput") chat_output["template"]["code"]["value"] = "changed !" + del chat_output["template"]["should_store_message"] graph_data = { "nodes": [ component_to_node("chat-input-1", "ChatInput", chat_input), @@ -140,3 +141,6 @@ async def test_refresh_starter_projects(): new_change = update_projects_components_with_latest_component_versions(graph_data, all_types) assert graph_data["nodes"][1]["data"]["node"]["template"]["code"]["value"] == "changed !" assert new_change["nodes"][1]["data"]["node"]["template"]["code"]["value"] != "changed !" + + assert "should_store_message" not in graph_data["nodes"][1]["data"]["node"]["template"] + assert "should_store_message" in new_change["nodes"][1]["data"]["node"]["template"] From 97c897b7534e71a25a6beded95140f302e63ec1d Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:14:02 -0300 Subject: [PATCH 03/14] fix: description of url component (#3105) Fixed description of URL component --- src/backend/base/langflow/components/data/URL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/data/URL.py b/src/backend/base/langflow/components/data/URL.py index f4e3c06433f6..967ab20cfcea 100644 --- a/src/backend/base/langflow/components/data/URL.py +++ b/src/backend/base/langflow/components/data/URL.py @@ -17,7 +17,7 @@ class URLComponent(Component): MessageTextInput( name="urls", display_name="URLs", - info="Enter one or more URLs, separated by commas.", + info="Enter one or more URLs, by clicking the '+' button.", is_list=True, ), ] From 7276f699fc85c611f1c3f83a19a368da9841e3a4 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 31 Jul 2024 11:24:57 -0300 Subject: [PATCH 04/14] feat: remove unnecessary condition in App component (#3107) The unnecessary condition in the App component was causing the error modal to not appear when there was an error in the health data. This commit removes the unnecessary condition to ensure that the error modal is displayed correctly. --- src/frontend/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index ca3447e97d0b..6fc41300cc37 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -137,7 +137,6 @@ export default function App() { message={FETCH_ERROR_MESSAGE} openModal={ isErrorHealth || - !healthData || (healthData && Object.values(healthData).some((value) => value !== "ok")) } From 5076f2014e9a596848f3ec4acc288de5bf8aa2b8 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:03:42 -0300 Subject: [PATCH 05/14] fix: flow experimental components (#3093) * refactor(utils.py): simplify data processing logic in build_data_from_result_data function for better readability and maintainability * feat: set default output type to "chat" in run_flow function * refactor(FlowTool.py): refactor FlowToolComponent class to inherit from LCToolComponent * Fixed Flow as Tool component * Fixed refresh button not appearing at the end * Added way of connecting SecretStrInput to messages * Added real_time_refresh when field has button update too * Refactored SubFlow component * Fixed FlowTool to only output tool and removed async function def * fix: two statements in the same line * [autofix.ci] apply automated fixes * Fixed lint issues * fixed dataobj with wrong name * changed tweaks dict type * Fixed margin appearing in output too * Fixed useless button that made styling worse on handlerendercomponent --------- Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../langflow/components/prototypes/SubFlow.py | 117 ++++++++---------- .../base/langflow/initial_setup/setup.py | 5 +- src/backend/base/langflow/inputs/inputs.py | 39 +++++- .../handleRenderComponent/index.tsx | 9 +- .../components/parameterComponent/index.tsx | 8 +- .../hooks/use-handle-new-value.tsx | 3 +- .../refreshParameterComponent/index.tsx | 2 +- 7 files changed, 104 insertions(+), 79 deletions(-) diff --git a/src/backend/base/langflow/components/prototypes/SubFlow.py b/src/backend/base/langflow/components/prototypes/SubFlow.py index f3f0b46232f2..009822fee806 100644 --- a/src/backend/base/langflow/components/prototypes/SubFlow.py +++ b/src/backend/base/langflow/components/prototypes/SubFlow.py @@ -1,29 +1,25 @@ from typing import Any, List, Optional +from loguru import logger + from langflow.base.flow_processing.utils import build_data_from_result_data -from langflow.custom import CustomComponent +from langflow.custom import Component from langflow.graph.graph.base import Graph -from langflow.graph.schema import RunOutputs from langflow.graph.vertex.base import Vertex from langflow.helpers.flow import get_flow_inputs -from langflow.schema import Data -from langflow.schema.dotdict import dotdict -from langflow.template.field.base import Input -from loguru import logger +from langflow.io import DropdownInput, Output +from langflow.schema import Data, dotdict -class SubFlowComponent(CustomComponent): +class SubFlowComponent(Component): display_name = "Sub Flow" - description = ( - "Dynamically Generates a Component from a Flow. The output is a list of data with keys 'result' and 'message'." - ) + description = "Generates a Component from a Flow, with all of its inputs, and " name = "SubFlow" beta: bool = True - field_order = ["flow_name"] def get_flow_names(self) -> List[str]: - flow_datas = self.list_flows() - return [flow_data.data["name"] for flow_data in flow_datas] + flow_data = self.list_flows() + return [flow_data.data["name"] for flow_data in flow_data] def get_flow(self, flow_name: str) -> Optional[Data]: flow_datas = self.list_flows() @@ -33,12 +29,11 @@ def get_flow(self, flow_name: str) -> Optional[Data]: return None def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): - logger.debug(f"Updating build config with field value {field_value} and field name {field_name}") if field_name == "flow_name": build_config["flow_name"]["options"] = self.get_flow_names() - # Clean up the build config + for key in list(build_config.keys()): - if key not in self.field_order + ["code", "_type", "get_final_results_only"]: + if key not in [x.name for x in self.inputs] + ["code", "_type", "get_final_results_only"]: del build_config[key] if field_value is not None and field_name == "flow_name": try: @@ -55,62 +50,58 @@ def update_build_config(self, build_config: dotdict, field_value: Any, field_nam return build_config - def add_inputs_to_build_config(self, inputs: List[Vertex], build_config: dotdict): - new_fields: list[Input] = [] - for vertex in inputs: - field = Input( - display_name=vertex.display_name, - name=vertex.id, - info=vertex.description, - field_type="str", - value=None, - ) - new_fields.append(field) - logger.debug(new_fields) + def add_inputs_to_build_config(self, inputs_vertex: List[Vertex], build_config: dotdict): + new_fields: list[dotdict] = [] + + for vertex in inputs_vertex: + new_vertex_inputs = [] + field_template = vertex.data["node"]["template"] + for inp in field_template.keys(): + if inp not in ["code", "_type"]: + field_template[inp]["display_name"] = ( + vertex.display_name + " - " + field_template[inp]["display_name"] + ) + field_template[inp]["name"] = vertex.id + "|" + inp + new_vertex_inputs.append(field_template[inp]) + new_fields += new_vertex_inputs for field in new_fields: - build_config[field.name] = field.to_dict() + build_config[field["name"]] = field return build_config - def build_config(self): - return { - "input_value": { - "display_name": "Input Value", - "multiline": True, - }, - "flow_name": { - "display_name": "Flow Name", - "info": "The name of the flow to run.", - "options": [], - "real_time_refresh": True, - "refresh_button": True, - }, - "tweaks": { - "display_name": "Tweaks", - "info": "Tweaks to apply to the flow.", - }, - "get_final_results_only": { - "display_name": "Get Final Results Only", - "info": "If False, the output will contain all outputs from the flow.", - "advanced": True, - }, - } - - async def build(self, flow_name: str, get_final_results_only: bool = True, **kwargs) -> List[Data]: - tweaks = {key: {"input_value": value} for key, value in kwargs.items()} - run_outputs: List[Optional[RunOutputs]] = await self.run_flow( + inputs = [ + DropdownInput( + name="flow_name", + display_name="Flow Name", + info="The name of the flow to run.", + options=[], + refresh_button=True, + real_time_refresh=True, + ), + ] + + outputs = [Output(name="flow_outputs", display_name="Flow Outputs", method="generate_results")] + + async def generate_results(self) -> List[Data]: + tweaks: dict = {} + for field in self._attributes.keys(): + if field != "flow_name": + [node, name] = field.split("|") + if node not in tweaks.keys(): + tweaks[node] = {} + tweaks[node][name] = self._attributes[field] + + run_outputs = await self.run_flow( tweaks=tweaks, - flow_name=flow_name, + flow_name=self.flow_name, + output_type="all", ) + data: list[Data] = [] if not run_outputs: - return [] + return data run_output = run_outputs[0] - data = [] if run_output is not None: for output in run_output.outputs: if output: - data.extend(build_data_from_result_data(output, get_final_results_only)) - - self.status = data - logger.debug(data) + data.extend(build_data_from_result_data(output)) return data diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index c4a43d4cf453..dcda6b50896d 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -343,7 +343,10 @@ def load_starter_projects() -> list[tuple[Path, dict]]: starter_projects = [] folder = Path(__file__).parent / "starter_projects" for file in folder.glob("*.json"): - project = orjson.loads(file.read_text(encoding="utf-8")) + try: + project = orjson.loads(file.read_text(encoding="utf-8")) + except orjson.JSONDecodeError as e: + raise ValueError(f"Error loading starter project {file}: {e}") starter_projects.append((file, project)) logger.info(f"Loaded starter project {file}") return starter_projects diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index c8023775f24a..4d616e175803 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -226,9 +226,46 @@ class SecretStrInput(BaseInputMixin, DatabaseLoadMixin): field_type: Optional[SerializableFieldTypes] = FieldTypes.PASSWORD password: CoalesceBool = Field(default=True) - input_types: list[str] = [] + input_types: list[str] = ["Message"] load_from_db: CoalesceBool = True + @field_validator("value") + @classmethod + def validate_value(cls, v: Any, _info): + """ + Validates the given value and returns the processed value. + + Args: + v (Any): The value to be validated. + _info: Additional information about the input. + + Returns: + The processed value. + + Raises: + ValueError: If the value is not of a valid type or if the input is missing a required key. + """ + value: str | AsyncIterator | Iterator | None = None + if isinstance(v, str): + value = v + elif isinstance(v, Message): + value = v.text + elif isinstance(v, Data): + if v.text_key in v.data: + value = v.data[v.text_key] + else: + keys = ", ".join(v.data.keys()) + input_name = _info.data["name"] + raise ValueError( + f"The input to '{input_name}' must contain the key '{v.text_key}'." + f"You can set `text_key` to one of the following keys: {keys} or set the value using another Component." + ) + elif isinstance(v, (AsyncIterator, Iterator)): + value = v + else: + raise ValueError(f"Invalid value type {type(v)}") + return value + class IntInput(BaseInputMixin, ListableInputMixin, RangeMixin, MetadataTraceMixin): """ diff --git a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx index d8b3b772e010..51b50332ed82 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/handleRenderComponent/index.tsx @@ -1,7 +1,5 @@ -import { title } from "process"; import { Handle, Position } from "reactflow"; import ShadTooltip from "../../../../components/shadTooltipComponent"; -import { Button } from "../../../../components/ui/button"; import { isValidConnection, scapedJSONStringfy, @@ -37,10 +35,7 @@ export default function HandleRenderComponent({ testIdComplement?: string; }) { return ( - + ); } diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 1fcc009d3cc7..611fd79be4e8 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -349,8 +349,8 @@ export default function ParameterComponent({ testIdComplement={`${data?.type?.toLowerCase()}-shownode`} /> )} -
- {data.node?.template[name] !== undefined && ( + {data.node?.template[name] !== undefined && ( +
- )} -
+
+ )} {openOutputModal && ( { options?.setNodeClass && options.setNodeClass(newNodeClass); diff --git a/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx index 8289143f69ba..796f9b768ba3 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx @@ -29,7 +29,7 @@ export function RefreshParameterComponent({ setErrorData, ); return ( -
+
{children} {templateData.refresh_button && (
From ca68414e6b41bc8a02df1386f421045b7da86ca9 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 31 Jul 2024 14:38:27 -0300 Subject: [PATCH 06/14] feat: update GenericNode to include lf_version in node data (#3112) * feat: update GenericNode to include lf_version in node data * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> --- .../src/CustomNodes/GenericNode/index.tsx | 19 ++++++++++++++++++- src/frontend/src/types/api/index.ts | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index f52cc514c444..ac85a7f7f0b3 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -65,7 +65,7 @@ export default function GenericNode({ const updateNodeInternals = useUpdateNodeInternals(); const setErrorData = useAlertStore((state) => state.setErrorData); const isDark = useDarkStore((state) => state.dark); - + const version = useDarkStore((state) => state.version); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const [inputName, setInputName] = useState(false); @@ -215,6 +215,23 @@ export default function GenericNode({ setShowNode(data.showNode ?? true); }, [data.showNode]); + useEffect(() => { + if (buildStatus === BuildStatus.BUILT && !isBuilding) { + setNode(data.id, (old) => { + return { + ...old, + data: { + ...old.data, + node: { + ...old.data.node, + lf_version: version, + }, + }, + }; + }); + } + }, [buildStatus, isBuilding]); + const [loadingUpdate, setLoadingUpdate] = useState(false); const [showHiddenOutputs, setShowHiddenOutputs] = useState(false); diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 315912e95dec..e4d6fade26b8 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -43,6 +43,7 @@ export type APIClassType = { official?: boolean; outputs?: Array; frozen?: boolean; + lf_version?: string; flow?: FlowType; field_order?: string[]; [key: string]: From f08467d84ba6558a5450fced31a2de8104a8bee8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:23:19 -0300 Subject: [PATCH 07/14] fix: handle PydanticSerializationError that generates generic errors in the UI (#3108) * fix(main.py): handle PydanticSerializationError in JavaScriptMIMETypeMiddleware to provide detailed error message in case of serialization error * fix(main.py): handle PydanticSerializationError in JavaScriptMIMETypeMiddleware to provide detailed error message in case of serialization error * improve error handling * feat: Handle PydanticSerializationError in JavaScriptMIMETypeMiddleware Refactor the `JavaScriptMIMETypeMiddleware` class in `main.py` to handle the `PydanticSerializationError` exception. This change ensures that a detailed error message is provided in case of a serialization error. The error message is now serialized as a JSON string and included in the `detail` field of the `HTTPException` raised. * feat: Add tryParseJson function to utils.ts This commit adds a new function called `tryParseJson` to the `utils.ts` file. The function attempts to parse a JSON string and returns the parsed object. If parsing fails, it returns undefined. This function can be used to safely parse JSON strings without throwing an error. * fix: Handle error message in buildVertex function This commit modifies the buildVertex function in buildUtils.ts to handle error messages more effectively. It adds logic to extract the error message from the AxiosError response and parse it as JSON if possible. If the error message is not an array, it converts it into an array. This change ensures that the error message is properly displayed when building a component fails. * remove console.log * remove not related code --------- Co-authored-by: anovazzi1 --- src/backend/base/langflow/main.py | 17 +++++++++++------ src/frontend/src/utils/buildUtils.ts | 14 ++++++++++---- src/frontend/src/utils/utils.ts | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/backend/base/langflow/main.py b/src/backend/base/langflow/main.py index 697f74c95abb..ca723ea299da 100644 --- a/src/backend/base/langflow/main.py +++ b/src/backend/base/langflow/main.py @@ -1,24 +1,26 @@ -import os import asyncio +import json +import os import warnings from contextlib import asynccontextmanager +from http import HTTPStatus from pathlib import Path from typing import Optional from urllib.parse import urlencode import nest_asyncio # type: ignore -from fastapi import FastAPI, Request, Response, HTTPException +from fastapi import FastAPI, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles -from http import HTTPStatus from loguru import logger +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from pydantic import PydanticDeprecatedSince20 +from pydantic_core import PydanticSerializationError from rich import print as rprint from starlette.middleware.base import BaseHTTPMiddleware -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor -from langflow.api import router, health_check_router, log_router +from langflow.api import health_check_router, log_router, router from langflow.initial_setup.setup import ( create_or_update_starter_projects, initialize_super_user_if_needed, @@ -67,7 +69,10 @@ async def dispatch(self, request: Request, call_next): try: response = await call_next(request) except Exception as exc: - logger.error(exc) + if isinstance(exc, PydanticSerializationError): + message = "Something went wrong while serializing the response. Please share this error on our GitHub repository." + error_messages = json.dumps([message, str(exc)]) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=error_messages) from exc raise exc if "files/" not in request.url.path and request.url.path.endswith(".js") and response.status_code == 200: response.headers["Content-Type"] = "text/javascript" diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 35e657180b7d..ad9ac48688fc 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -7,6 +7,7 @@ import useFlowStore from "../stores/flowStore"; import { VertexBuildTypeAPI } from "../types/api"; import { isErrorLogType } from "../types/utils/typeCheckingUtils"; import { VertexLayerElementType } from "../types/zustand/flow"; +import { tryParseJson } from "./utils"; type BuildVerticesParams = { setLockChat?: (lock: boolean) => void; @@ -306,12 +307,17 @@ async function buildVertex({ buildResults.push(buildData.valid); } catch (error) { console.error(error); + let errorMessage: string | string[] = + (error as AxiosError).response?.data?.detail || + (error as AxiosError).response?.data?.message || + "An unexpected error occurred while building the Component. Please try again."; + errorMessage = tryParseJson(errorMessage as string) ?? errorMessage; + if (!Array.isArray(errorMessage)) { + errorMessage = [errorMessage]; + } onBuildError!( "Error Building Component", - [ - (error as AxiosError).response?.data?.detail ?? - "An unexpected error occurred while building the Component. Please try again.", - ], + errorMessage, verticesIds.map((id) => ({ id })), ); buildResults.push(false); diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 313fe77be8b4..8b3bd0336e47 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -546,3 +546,19 @@ export function generateBackendColumnsFromValue(rows: Object[]): ColumnField[] { return newColumn; }); } + +/** + * Tries to parse a JSON string and returns the parsed object. + * If parsing fails, returns undefined. + * + * @param json - The JSON string to parse. + * @returns The parsed JSON object, or undefined if parsing fails. + */ +export function tryParseJson(json: string) { + try { + const parsedJson = JSON.parse(json); + return parsedJson; + } catch (error) { + return; + } +} From e0afe904102962aa4c6e39bee30c97e064ec4e51 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 15:23:34 -0300 Subject: [PATCH 08/14] refactor: update code references to use _code instead of code (#3113) --- src/backend/base/langflow/api/v1/endpoints.py | 4 +- .../custom/custom_component/base_component.py | 11 ++-- .../custom_component/custom_component.py | 10 ++-- .../directory_reader/directory_reader.py | 2 +- src/backend/base/langflow/custom/utils.py | 18 +++---- .../tests/unit/test_custom_component.py | 50 +++++++++---------- .../tests/unit/test_helper_components.py | 2 +- 7 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index fadd1d7b313a..296bfbba264c 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -552,7 +552,7 @@ async def custom_component( raw_code: CustomComponentRequest, user: User = Depends(get_current_active_user), ): - component = Component(code=raw_code.code) + component = Component(_code=raw_code.code) built_frontend_node, component_instance = build_custom_component_template(component, user_id=user.id) if raw_code.frontend_node is not None: @@ -582,7 +582,7 @@ async def custom_component_update( """ try: - component = Component(code=code_request.code) + component = Component(_code=code_request.code) component_node, cc_instance = build_custom_component_template( component, diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index 098942dd4182..ce1c5c445b44 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -23,7 +23,8 @@ class BaseComponent: ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided." ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided." - code: Optional[str] = None + _code: Optional[str] = None + """The code of the component. Defaults to None.""" _function_entrypoint_name: str = "build" field_config: dict = {} _user_id: Optional[str] @@ -47,7 +48,7 @@ def get_code_tree(self, code: str): return parser.parse_code() def get_function(self): - if not self.code: + if not self._code: raise ComponentCodeNullError( status_code=400, detail={"error": self.ERROR_CODE_NULL, "traceback": ""}, @@ -62,7 +63,7 @@ def get_function(self): }, ) - return validate.create_function(self.code, self._function_entrypoint_name) + return validate.create_function(self._code, self._function_entrypoint_name) def build_template_config(self) -> dict: """ @@ -71,10 +72,10 @@ def build_template_config(self) -> dict: Returns: A dictionary representing the template configuration. """ - if not self.code: + if not self._code: return {} - cc_class = eval_custom_component_code(self.code) + cc_class = eval_custom_component_code(self._code) component_instance = cc_class() template_config = {} diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index c5e10932f724..95c10022f033 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -64,8 +64,6 @@ class CustomComponent(BaseComponent): is_output: Optional[bool] = None """The output state of the component. Defaults to None. If True, the component must have a field named 'input_value'.""" - code: Optional[str] = None - """The code of the component. Defaults to None.""" field_config: dict = {} """The field configuration of the component. Defaults to an empty dictionary.""" field_order: Optional[List[str]] = None @@ -226,7 +224,7 @@ def tree(self): Returns: dict: The code tree of the custom component. """ - return self.get_code_tree(self.code or "") + return self.get_code_tree(self._code or "") def to_data(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Data]: """ @@ -326,7 +324,7 @@ def get_method(self, method_name: str): Returns: dict: The build method for the custom component. """ - if not self.code: + if not self._code: return {} component_classes = [ @@ -379,7 +377,7 @@ def get_main_class_name(self): Returns: str: The main class name of the custom component. """ - if not self.code: + if not self._code: return "" base_name = self.code_class_base_inheritance @@ -468,7 +466,7 @@ def get_function(self): Returns: Callable: The function associated with the custom component. """ - return validate.create_function(self.code, self.function_entrypoint_name) + return validate.create_function(self._code, self.function_entrypoint_name) async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> "Graph": if not self._user_id: diff --git a/src/backend/base/langflow/custom/directory_reader/directory_reader.py b/src/backend/base/langflow/custom/directory_reader/directory_reader.py index 1fb4ca93e5a8..18f39b5eb039 100644 --- a/src/backend/base/langflow/custom/directory_reader/directory_reader.py +++ b/src/backend/base/langflow/custom/directory_reader/directory_reader.py @@ -373,7 +373,7 @@ def get_output_types_from_code(code: str) -> list: """ Get the output types from the code. """ - custom_component = CustomComponent(code=code) + custom_component = CustomComponent(_code=code) types_list = custom_component.get_function_entrypoint_return_type # Get the name of types classes diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index d721bc275dc8..84aef23994ff 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -266,10 +266,10 @@ def run_build_inputs( def get_component_instance(custom_component: CustomComponent, user_id: Optional[Union[str, UUID]] = None): try: - if custom_component.code is None: + if custom_component._code is None: raise ValueError("Code is None") - elif isinstance(custom_component.code, str): - custom_class = eval_custom_component_code(custom_component.code) + elif isinstance(custom_component._code, str): + custom_class = eval_custom_component_code(custom_component._code) else: raise ValueError("Invalid code type") except Exception as exc: @@ -300,10 +300,10 @@ def run_build_config( """Build the field configuration for a custom component""" try: - if custom_component.code is None: + if custom_component._code is None: raise ValueError("Code is None") - elif isinstance(custom_component.code, str): - custom_class = eval_custom_component_code(custom_component.code) + elif isinstance(custom_component._code, str): + custom_class = eval_custom_component_code(custom_component._code) else: raise ValueError("Invalid code type") except Exception as exc: @@ -363,7 +363,7 @@ def build_custom_component_template_from_inputs( # The List of Inputs fills the role of the build_config and the entrypoint_args field_config = custom_component.template_config frontend_node = ComponentFrontendNode.from_inputs(**field_config) - frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) + frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) # But we now need to calculate the return_type of the methods in the outputs for output in frontend_node.outputs: if output.types: @@ -407,7 +407,7 @@ def build_custom_component_template( add_extra_fields(frontend_node, field_config, entrypoint_args) - frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {})) + frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type) add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type) @@ -432,7 +432,7 @@ def create_component_template(component): component_code = component["code"] component_output_types = component["output_types"] - component_extractor = Component(code=component_code) + component_extractor = Component(_code=component_code) component_template, component_instance = build_custom_component_template(component_extractor) if not component_template["output_types"] and component_output_types: diff --git a/src/backend/tests/unit/test_custom_component.py b/src/backend/tests/unit/test_custom_component.py index a0e5525cb57a..d582f2933450 100644 --- a/src/backend/tests/unit/test_custom_component.py +++ b/src/backend/tests/unit/test_custom_component.py @@ -16,7 +16,7 @@ def code_component_with_multiple_outputs(): with open("src/backend/tests/data/component_multiple_outputs.py", "r") as f: code = f.read() - return Component(code=code) + return Component(_code=code) code_default = """ @@ -72,8 +72,8 @@ def test_component_init(): """ Test the initialization of the Component class. """ - component = BaseComponent(code=code_default, function_entrypoint_name="build") - assert component.code == code_default + component = BaseComponent(_code=code_default, function_entrypoint_name="build") + assert component._code == code_default assert component.function_entrypoint_name == "build" @@ -81,8 +81,8 @@ def test_component_get_code_tree(): """ Test the get_code_tree method of the Component class. """ - component = BaseComponent(code=code_default, function_entrypoint_name="build") - tree = component.get_code_tree(component.code) + component = BaseComponent(_code=code_default, function_entrypoint_name="build") + tree = component.get_code_tree(component._code) assert "imports" in tree @@ -91,7 +91,7 @@ def test_component_code_null_error(): Test the get_function method raises the ComponentCodeNullError when the code is empty. """ - component = BaseComponent(code="", function_entrypoint_name="") + component = BaseComponent(_code="", function_entrypoint_name="") with pytest.raises(ComponentCodeNullError): component.get_function() @@ -102,8 +102,8 @@ def test_custom_component_init(): """ function_entrypoint_name = "build" - custom_component = CustomComponent(code=code_default, function_entrypoint_name=function_entrypoint_name) - assert custom_component.code == code_default + custom_component = CustomComponent(_code=code_default, function_entrypoint_name=function_entrypoint_name) + assert custom_component._code == code_default assert custom_component.function_entrypoint_name == function_entrypoint_name @@ -111,7 +111,7 @@ def test_custom_component_build_template_config(): """ Test the build_template_config property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") config = custom_component.build_template_config() assert isinstance(config, dict) @@ -120,7 +120,7 @@ def test_custom_component_get_function(): """ Test the get_function property of the CustomComponent class. """ - custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build") + custom_component = CustomComponent(_code="def build(): pass", function_entrypoint_name="build") my_function = custom_component.get_function() assert isinstance(my_function, types.FunctionType) @@ -195,7 +195,7 @@ def test_component_get_function_valid(): Test the get_function method of the Component class with valid code and function_entrypoint_name. """ - component = BaseComponent(code="def build(): pass", function_entrypoint_name="build") + component = BaseComponent(_code="def build(): pass", function_entrypoint_name="build") my_function = component.get_function() assert callable(my_function) @@ -205,7 +205,7 @@ def test_custom_component_get_function_entrypoint_args(): Test the get_function_entrypoint_args property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") args = custom_component.get_function_entrypoint_args assert len(args) == 3 assert args[0]["name"] == "self" @@ -219,7 +219,7 @@ def test_custom_component_get_function_entrypoint_return_type(): property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") return_type = custom_component.get_function_entrypoint_return_type assert return_type == [Document] @@ -228,7 +228,7 @@ def test_custom_component_get_main_class_name(): """ Test the get_main_class_name property of the CustomComponent class. """ - custom_component = CustomComponent(code=code_default, function_entrypoint_name="build") + custom_component = CustomComponent(_code=code_default, function_entrypoint_name="build") class_name = custom_component.get_main_class_name assert class_name == "YourComponent" @@ -238,7 +238,7 @@ def test_custom_component_get_function_valid(): Test the get_function property of the CustomComponent class with valid code and function_entrypoint_name. """ - custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build") + custom_component = CustomComponent(_code="def build(): pass", function_entrypoint_name="build") my_function = custom_component.get_function assert callable(my_function) @@ -352,9 +352,9 @@ def test_component_get_code_tree_syntax_error(): Test the get_code_tree method of the Component class raises the CodeSyntaxError when given incorrect syntax. """ - component = BaseComponent(code="import os as", function_entrypoint_name="build") + component = BaseComponent(_code="import os as", function_entrypoint_name="build") with pytest.raises(CodeSyntaxError): - component.get_code_tree(component.code) + component.get_code_tree(component._code) def test_custom_component_class_template_validation_no_code(): @@ -362,7 +362,7 @@ def test_custom_component_class_template_validation_no_code(): Test the _class_template_validation method of the CustomComponent class raises the HTTPException when the code is None. """ - custom_component = CustomComponent(code=None, function_entrypoint_name="build") + custom_component = CustomComponent(_code=None, function_entrypoint_name="build") with pytest.raises(TypeError): custom_component.get_function() @@ -372,9 +372,9 @@ def test_custom_component_get_code_tree_syntax_error(): Test the get_code_tree method of the CustomComponent class raises the CodeSyntaxError when given incorrect syntax. """ - custom_component = CustomComponent(code="import os as", function_entrypoint_name="build") + custom_component = CustomComponent(_code="import os as", function_entrypoint_name="build") with pytest.raises(CodeSyntaxError): - custom_component.get_code_tree(custom_component.code) + custom_component.get_code_tree(custom_component._code) def test_custom_component_get_function_entrypoint_args_no_args(): @@ -387,7 +387,7 @@ class MyMainClass(CustomComponent): def build(): pass""" - custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") + custom_component = CustomComponent(_code=my_code, function_entrypoint_name="build") args = custom_component.get_function_entrypoint_args assert len(args) == 0 @@ -402,7 +402,7 @@ class MyClass(CustomComponent): def build(): pass""" - custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") + custom_component = CustomComponent(_code=my_code, function_entrypoint_name="build") return_type = custom_component.get_function_entrypoint_return_type assert return_type == [] @@ -416,7 +416,7 @@ def test_custom_component_get_main_class_name_no_main_class(): def build(): pass""" - custom_component = CustomComponent(code=my_code, function_entrypoint_name="build") + custom_component = CustomComponent(_code=my_code, function_entrypoint_name="build") class_name = custom_component.get_main_class_name assert class_name == "" @@ -426,13 +426,13 @@ def test_custom_component_build_not_implemented(): Test the build method of the CustomComponent class raises the NotImplementedError. """ - custom_component = CustomComponent(code="def build(): pass", function_entrypoint_name="build") + custom_component = CustomComponent(_code="def build(): pass", function_entrypoint_name="build") with pytest.raises(NotImplementedError): custom_component.build() def test_build_config_no_code(): - component = CustomComponent(code=None) + component = CustomComponent(_code=None) assert component.get_function_entrypoint_args == [] assert component.get_function_entrypoint_return_type == [] diff --git a/src/backend/tests/unit/test_helper_components.py b/src/backend/tests/unit/test_helper_components.py index dc07c583549c..75395b8620f5 100644 --- a/src/backend/tests/unit/test_helper_components.py +++ b/src/backend/tests/unit/test_helper_components.py @@ -32,7 +32,7 @@ def test_uuid_generator_component(): # Arrange uuid_generator_component = helpers.IDGeneratorComponent() - uuid_generator_component.code = open(helpers.IDGenerator.__file__, "r").read() + uuid_generator_component._code = open(helpers.IDGenerator.__file__, "r").read() frontend_node, _ = build_custom_component_template(uuid_generator_component) From 61c921e1ae6238b411f3062f0f4b8a5938bfa4fd Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa <72977554+Cristhianzl@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:49:54 -0300 Subject: [PATCH 09/14] refactor: add useQuery management on globalVariables CRUD (#3121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ (App.tsx): Add useGetGlobalVariables hook to fetch global variables data 🔧 (addNewVariableButton.tsx): Replace registerGlobalVariable function with usePostGlobalVariables hook for adding new global variables 🔧 (inputGlobalComponent/index.tsx): Replace deleteGlobalVariable function with useDeleteGlobalVariables hook for deleting global variables 🔧 (authContext.tsx): Remove unused getGlobalVariables function and setGlobalVariables state from authContext 🔧 (constants.ts): Add VARIABLES constant for global variables endpoint 🔧 (index.ts): Remove getGlobalVariables, registerGlobalVariable, and deleteGlobalVariable functions from API index ✨ (variables/index.ts): Add use-delete-global-variables hook for deleting global variables 🔧 (use-delete-global-variables.ts): Implement useDeleteGlobalVariables hook for deleting global variables ✨ (use-get-global-variables.ts): Add functionality to fetch and set global variables from API ✨ (use-patch-global-variables.ts): Add functionality to update global variables via API patch request ✨ (use-post-global-variables.ts): Add functionality to create new global variables via API post request 📝 (index.tsx): Import useGetGlobalVariables hook to fetch global variables on login page 📝 (index.tsx): Call mutateGetGlobalVariables function to fetch global variables on successful login 📝 (index.tsx): Import useDeleteGlobalVariables hook to delete global variables on global variables page 📝 (index.tsx): Call mutateDeleteGlobalVariable function to delete selected global variables 📝 (index.tsx): Handle success and error cases for deleting global variables * Removed console.log --------- Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> --- src/frontend/src/App.tsx | 5 ++ .../addNewVariableButton.tsx | 23 +++++--- .../components/inputGlobalComponent/index.tsx | 35 ++++++------ src/frontend/src/contexts/authContext.tsx | 14 ++--- .../src/controllers/API/helpers/constants.ts | 1 + src/frontend/src/controllers/API/index.ts | 47 ---------------- .../API/queries/variables/index.ts | 4 ++ .../variables/use-delete-global-variables.ts | 31 +++++++++++ .../variables/use-get-global-variables.ts | 54 +++++++++++++++++++ .../variables/use-patch-global-variables.ts | 38 +++++++++++++ .../variables/use-post-global-variables.ts | 40 ++++++++++++++ .../src/pages/AdminPage/LoginPage/index.tsx | 5 ++ .../pages/GlobalVariablesPage/index.tsx | 33 ++++++------ 13 files changed, 236 insertions(+), 94 deletions(-) create mode 100644 src/frontend/src/controllers/API/queries/variables/index.ts create mode 100644 src/frontend/src/controllers/API/queries/variables/use-delete-global-variables.ts create mode 100644 src/frontend/src/controllers/API/queries/variables/use-get-global-variables.ts create mode 100644 src/frontend/src/controllers/API/queries/variables/use-patch-global-variables.ts create mode 100644 src/frontend/src/controllers/API/queries/variables/use-post-global-variables.ts diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 6fc41300cc37..3485d47bffde 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -16,6 +16,7 @@ import { import { AuthContext } from "./contexts/authContext"; import { autoLogin } from "./controllers/API"; import { useGetHealthQuery } from "./controllers/API/queries/health"; +import { useGetGlobalVariables } from "./controllers/API/queries/variables"; import { useGetVersionQuery } from "./controllers/API/queries/version"; import { setupAxiosDefaults } from "./controllers/API/utils"; import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path"; @@ -42,6 +43,8 @@ export default function App() { const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders); + const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables(); + const { data: healthData, isFetching: fetchingHealth, @@ -66,6 +69,7 @@ export default function App() { if (user && user["access_token"]) { user["refresh_token"] = "auto"; login(user["access_token"], "auto"); + mutateGetGlobalVariables(); setUserData(user); setAutoLogin(true); fetchAllData(); @@ -99,6 +103,7 @@ export default function App() { */ return () => abortController.abort(); }, []); + const fetchAllData = async () => { setTimeout(async () => { await Promise.all([refreshStars(), fetchData()]); diff --git a/src/frontend/src/components/addNewVariableButtonComponent/addNewVariableButton.tsx b/src/frontend/src/components/addNewVariableButtonComponent/addNewVariableButton.tsx index 4dcda2deb9dc..a2aff271393c 100644 --- a/src/frontend/src/components/addNewVariableButtonComponent/addNewVariableButton.tsx +++ b/src/frontend/src/components/addNewVariableButtonComponent/addNewVariableButton.tsx @@ -1,5 +1,5 @@ +import { usePostGlobalVariables } from "@/controllers/API/queries/variables"; import { useState } from "react"; -import { registerGlobalVariable } from "../../controllers/API"; import BaseModal from "../../modals/baseModal"; import useAlertStore from "../../stores/alertStore"; import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables"; @@ -28,6 +28,7 @@ export default function AddNewVariableButton({ const [open, setOpen] = useState(false); const setErrorData = useAlertStore((state) => state.setErrorData); const componentFields = useTypesStore((state) => state.ComponentFields); + const unavaliableFields = new Set( Object.keys( useGlobalVariablesStore((state) => state.unavaliableFields) ?? {}, @@ -46,6 +47,9 @@ export default function AddNewVariableButton({ (state) => state.addGlobalVariable, ); + const { mutate: mutateAddGlobalVariable } = usePostGlobalVariables(); + const setSuccessData = useAlertStore((state) => state.setSuccessData); + function handleSaveVariable() { let data: { name: string; @@ -58,8 +62,9 @@ export default function AddNewVariableButton({ value, default_fields: fields, }; - registerGlobalVariable(data) - .then((res) => { + + mutateAddGlobalVariable(data, { + onSuccess: (res) => { const { name, id, type } = res.data; addGlobalVariable(name, id, type, fields); setKey(""); @@ -67,8 +72,12 @@ export default function AddNewVariableButton({ setType(""); setFields([]); setOpen(false); - }) - .catch((error) => { + + setSuccessData({ + title: `Variable ${name} created successfully`, + }); + }, + onError: (error) => { let responseError = error as ResponseErrorDetailAPI; setErrorData({ title: "Error creating variable", @@ -77,8 +86,10 @@ export default function AddNewVariableButton({ "An unexpected error occurred while adding a new variable. Please try again.", ], }); - }); + }, + }); } + return ( state.setErrorData); + const { mutate: mutateDeleteGlobalVariable } = useDeleteGlobalVariables(); + useEffect(() => { if (data && globalVariablesEntries) if (data.load_from_db && !globalVariablesEntries.includes(data.value)) { @@ -38,19 +39,23 @@ export default function InputGlobalComponent({ async function handleDelete(key: string) { const id = getVariableId(key); if (id !== undefined) { - await deleteGlobalVariable(id) - .then(() => { - removeGlobalVariable(key); - if (data?.value === key && data?.load_from_db) { - onChange("", false); - } - }) - .catch(() => { - setErrorData({ - title: "Error deleting variable", - list: [cn("ID not found for variable: ", key)], - }); - }); + mutateDeleteGlobalVariable( + { id }, + { + onSuccess: () => { + removeGlobalVariable(key); + if (data?.value === key && data?.load_from_db) { + onChange("", false); + } + }, + onError: () => { + setErrorData({ + title: "Error deleting variable", + list: [cn("ID not found for variable: ", key)], + }); + }, + }, + ); } else { setErrorData({ title: "Error deleting variable", diff --git a/src/frontend/src/contexts/authContext.tsx b/src/frontend/src/contexts/authContext.tsx index 3521fe8b69a2..907ca0fd65b6 100644 --- a/src/frontend/src/contexts/authContext.tsx +++ b/src/frontend/src/contexts/authContext.tsx @@ -8,14 +8,9 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore"; import { createContext, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import Cookies from "universal-cookie"; -import { - getGlobalVariables, - getLoggedUser, - requestLogout, -} from "../controllers/API"; +import { getLoggedUser, requestLogout } from "../controllers/API"; import useAlertStore from "../stores/alertStore"; import { useFolderStore } from "../stores/foldersStore"; -import { useGlobalVariablesStore } from "../stores/globalVariablesStore/globalVariables"; import { useStoreStore } from "../stores/storeStore"; import { Users } from "../types/api"; import { AuthContextType } from "../types/contexts/auth"; @@ -48,9 +43,7 @@ export function AuthProvider({ children }): React.ReactElement { ); const getFoldersApi = useFolderStore((state) => state.getFoldersApi); - const setGlobalVariables = useGlobalVariablesStore( - (state) => state.setGlobalVariables, - ); + const checkHasStore = useStoreStore((state) => state.checkHasStore); const fetchApiData = useStoreStore((state) => state.fetchApiData); const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows); @@ -77,8 +70,7 @@ export function AuthProvider({ children }): React.ReactElement { const isSuperUser = user!.is_superuser; useAuthStore.getState().setIsAdmin(isSuperUser); getFoldersApi(true, true); - const res = await getGlobalVariables(); - setGlobalVariables(res); + checkHasStore(); fetchApiData(); }) diff --git a/src/frontend/src/controllers/API/helpers/constants.ts b/src/frontend/src/controllers/API/helpers/constants.ts index 2212023f0a98..4f54d7a78849 100644 --- a/src/frontend/src/controllers/API/helpers/constants.ts +++ b/src/frontend/src/controllers/API/helpers/constants.ts @@ -17,6 +17,7 @@ export const URLs = { CUSTOM_COMPONENT: `custom_component`, FLOWS: `flows`, FOLDERS: `folders`, + VARIABLES: `variables`, } as const; export function getURL(key: keyof typeof URLs, params: any = {}) { diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 2b66670998e6..52ca5ba55a16 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -818,53 +818,6 @@ export async function requestLogout() { } } -export async function getGlobalVariables(): Promise<{ - [key: string]: { id: string; type: string; default_fields: string[] }; -}> { - const globalVariables = {}; - (await api.get(`${BASE_URL_API}variables/`))?.data?.forEach((element) => { - globalVariables[element.name] = { - id: element.id, - type: element.type, - default_fields: element.default_fields, - }; - }); - return globalVariables; -} - -export async function registerGlobalVariable({ - name, - value, - type, - default_fields = [], -}: { - name: string; - value: string; - type?: string; - default_fields?: string[]; -}): Promise> { - try { - const response = await api.post(`${BASE_URL_API}variables/`, { - name, - value, - type, - default_fields: default_fields, - }); - return response; - } catch (error) { - throw error; - } -} - -export async function deleteGlobalVariable(id: string) { - try { - const response = await api.delete(`${BASE_URL_API}variables/${id}`); - return response; - } catch (error) { - throw error; - } -} - export async function updateGlobalVariable( name: string, value: string, diff --git a/src/frontend/src/controllers/API/queries/variables/index.ts b/src/frontend/src/controllers/API/queries/variables/index.ts new file mode 100644 index 000000000000..47bccb23ceb1 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/variables/index.ts @@ -0,0 +1,4 @@ +export * from "./use-delete-global-variables"; +export * from "./use-get-global-variables"; +export * from "./use-patch-global-variables"; +export * from "./use-post-global-variables"; diff --git a/src/frontend/src/controllers/API/queries/variables/use-delete-global-variables.ts b/src/frontend/src/controllers/API/queries/variables/use-delete-global-variables.ts new file mode 100644 index 000000000000..510956beeeea --- /dev/null +++ b/src/frontend/src/controllers/API/queries/variables/use-delete-global-variables.ts @@ -0,0 +1,31 @@ +import { useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface DeleteGlobalVariablesParams { + id: string | undefined; +} + +export const useDeleteGlobalVariables: useMutationFunctionType< + undefined, + DeleteGlobalVariablesParams +> = (options?) => { + const { mutate } = UseRequestProcessor(); + + const deleteGlobalVariables = async ({ + id, + }: DeleteGlobalVariablesParams): Promise => { + const res = await api.delete(`${getURL("VARIABLES")}/${id}`); + return res.data; + }; + + const mutation: UseMutationResult< + DeleteGlobalVariablesParams, + any, + DeleteGlobalVariablesParams + > = mutate(["useDeleteGlobalVariables"], deleteGlobalVariables, options); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/variables/use-get-global-variables.ts b/src/frontend/src/controllers/API/queries/variables/use-get-global-variables.ts new file mode 100644 index 000000000000..5b5df35cf91e --- /dev/null +++ b/src/frontend/src/controllers/API/queries/variables/use-get-global-variables.ts @@ -0,0 +1,54 @@ +import { useGlobalVariablesStore } from "@/stores/globalVariablesStore/globalVariables"; +import { useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +type GlobalVariable = { + id: string; + type: string; + default_fields: string[]; + name: string; +}; + +export const useGetGlobalVariables: useMutationFunctionType = ( + options?, +) => { + const { mutate } = UseRequestProcessor(); + + const setGlobalVariables = useGlobalVariablesStore( + (state) => state.setGlobalVariables, + ); + + const getGlobalVariables = async (): Promise<[GlobalVariable]> => { + const res = await api.get(`${getURL("VARIABLES")}/`); + return res.data; + }; + + const getGlobalVariablesFn = async (): Promise<{ + [key: string]: GlobalVariable; + }> => { + const data = await getGlobalVariables(); + const globalVariables = {}; + + data?.forEach((element) => { + globalVariables[element.name] = { + id: element.id, + type: element.type, + default_fields: element.default_fields, + }; + }); + + setGlobalVariables(globalVariables); + return globalVariables; + }; + + const mutation: UseMutationResult = mutate( + ["useGetGlobalVariables"], + getGlobalVariablesFn, + options, + ); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/variables/use-patch-global-variables.ts b/src/frontend/src/controllers/API/queries/variables/use-patch-global-variables.ts new file mode 100644 index 000000000000..bc2eac5e19cc --- /dev/null +++ b/src/frontend/src/controllers/API/queries/variables/use-patch-global-variables.ts @@ -0,0 +1,38 @@ +import { changeUser, useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface PatchGlobalVariablesParams { + name: string; + value: string; + id: string; +} + +export const usePatchGlobalVariables: useMutationFunctionType< + undefined, + PatchGlobalVariablesParams +> = (options?) => { + const { mutate } = UseRequestProcessor(); + + async function patchGlobalVariables({ + name, + value, + id, + }: PatchGlobalVariablesParams): Promise { + const res = await api.patch(`${getURL("VARIABLES")}/${id}`, { + name, + value, + }); + return res.data; + } + + const mutation: UseMutationResult< + PatchGlobalVariablesParams, + any, + PatchGlobalVariablesParams + > = mutate(["usePatchGlobalVariables"], patchGlobalVariables, options); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/variables/use-post-global-variables.ts b/src/frontend/src/controllers/API/queries/variables/use-post-global-variables.ts new file mode 100644 index 000000000000..937954accb82 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/variables/use-post-global-variables.ts @@ -0,0 +1,40 @@ +import { useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface PostGlobalVariablesParams { + name: string; + value: string; + type?: string; + default_fields?: string[]; +} + +export const usePostGlobalVariables: useMutationFunctionType< + undefined, + PostGlobalVariablesParams +> = (options?) => { + const { mutate } = UseRequestProcessor(); + + const postGlobalVariablesFunction = async ({ + name, + value, + type, + default_fields = [], + }): Promise> => { + const res = await api.post(`${getURL("VARIABLES")}`, { + name, + value, + type, + default_fields: default_fields, + }); + return res; + }; + + const mutation: UseMutationResult = + mutate(["usePostGlobalVariables"], postGlobalVariablesFunction, options); + + return mutation; +}; diff --git a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx index c6ce32c30d2e..e4cccd574b8f 100644 --- a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx +++ b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx @@ -1,3 +1,4 @@ +import { useGetGlobalVariables } from "@/controllers/API/queries/variables"; import { useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "../../../components/ui/button"; @@ -21,6 +22,8 @@ export default function LoginAdminPage() { const { login } = useContext(AuthContext); const setLoading = useAlertStore((state) => state.setLoading); + const { mutate: mutateGetGlobalVariables } = useGetGlobalVariables(); + const { password, username } = inputState; const setErrorData = useAlertStore((state) => state.setErrorData); function handleInput({ @@ -41,6 +44,8 @@ export default function LoginAdminPage() { setLoading(true); login(user.access_token, "login"); + mutateGetGlobalVariables(); + navigate("/admin/"); }) .catch((error) => { diff --git a/src/frontend/src/pages/SettingsPage/pages/GlobalVariablesPage/index.tsx b/src/frontend/src/pages/SettingsPage/pages/GlobalVariablesPage/index.tsx index 6f0dac836c98..51d3fa3f90f5 100644 --- a/src/frontend/src/pages/SettingsPage/pages/GlobalVariablesPage/index.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/GlobalVariablesPage/index.tsx @@ -1,6 +1,7 @@ import IconComponent from "../../../../components/genericIconComponent"; import { Button } from "../../../../components/ui/button"; +import { useDeleteGlobalVariables } from "@/controllers/API/queries/variables"; import { ColDef, ColGroupDef, SelectionChangedEvent } from "ag-grid-community"; import { useEffect, useState } from "react"; import AddNewVariableButton from "../../../../components/addNewVariableButtonComponent/addNewVariableButton"; @@ -8,7 +9,6 @@ import Dropdown from "../../../../components/dropdownComponent"; import ForwardedIconComponent from "../../../../components/genericIconComponent"; import TableComponent from "../../../../components/tableComponent"; import { Badge } from "../../../../components/ui/badge"; -import { deleteGlobalVariable } from "../../../../controllers/API"; import useAlertStore from "../../../../stores/alertStore"; import { useGlobalVariablesStore } from "../../../../stores/globalVariablesStore/globalVariables"; @@ -108,23 +108,26 @@ export default function GlobalVariablesPage() { const [selectedRows, setSelectedRows] = useState([]); + const { mutate: mutateDeleteGlobalVariable } = useDeleteGlobalVariables(); + async function removeVariables() { - const deleteGlobalVariablesPromise = selectedRows.map(async (row) => { + selectedRows.map(async (row) => { const id = getVariableId(row); - const deleteGlobalVariables = deleteGlobalVariable(id!); - await deleteGlobalVariables; + mutateDeleteGlobalVariable( + { id }, + { + onSuccess: () => { + removeGlobalVariable(row); + }, + onError: () => { + setErrorData({ + title: `Error deleting variable`, + list: [`ID not found for variable: ${row}`], + }); + }, + }, + ); }); - Promise.all(deleteGlobalVariablesPromise) - .then(() => { - selectedRows.forEach((row) => { - removeGlobalVariable(row); - }); - }) - .catch(() => { - setErrorData({ - title: `Error deleting global variables.`, - }); - }); } return ( From d7aed90e5dd771963bcdadf40e852a35ecb9906b Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa <72977554+Cristhianzl@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:50:10 -0300 Subject: [PATCH 10/14] bugfix: check if user exists before create a new refresh token (#3076) check if user exists before create a new refresh token --- src/backend/base/langflow/api/v1/login.py | 3 +- .../base/langflow/services/auth/utils.py | 8 +++- src/frontend/src/controllers/API/api.tsx | 43 +++++++++++-------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/backend/base/langflow/api/v1/login.py b/src/backend/base/langflow/api/v1/login.py index 92370cbcee4d..a966ea446808 100644 --- a/src/backend/base/langflow/api/v1/login.py +++ b/src/backend/base/langflow/api/v1/login.py @@ -129,13 +129,14 @@ async def refresh_token( request: Request, response: Response, settings_service: "SettingsService" = Depends(get_settings_service), + db: Session = Depends(get_session), ): auth_settings = settings_service.auth_settings token = request.cookies.get("refresh_token_lf") if token: - tokens = create_refresh_token(token) + tokens = create_refresh_token(token, db) response.set_cookie( "refresh_token_lf", tokens["refresh_token"], diff --git a/src/backend/base/langflow/services/auth/utils.py b/src/backend/base/langflow/services/auth/utils.py index 7db158b019c8..bf12c9a91bc9 100644 --- a/src/backend/base/langflow/services/auth/utils.py +++ b/src/backend/base/langflow/services/auth/utils.py @@ -305,7 +305,13 @@ def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)) ) user_id: UUID = payload.get("sub") # type: ignore token_type: str = payload.get("type") # type: ignore - if user_id is None or token_type is None: + + if user_id is None or token_type == "": + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") + + user_exists = get_user_by_id(db, user_id) + + if user_exists is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") return create_user_tokens(user_id, db) diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx index d6a89901c292..66b7334ae94a 100644 --- a/src/frontend/src/controllers/API/api.tsx +++ b/src/frontend/src/controllers/API/api.tsx @@ -46,18 +46,18 @@ function ApiInterceptor() { await tryToRenewAccessToken(error); const accessToken = cookies.get(LANGFLOW_ACCESS_TOKEN); - if (!accessToken && error?.config?.url?.includes("login")) { return Promise.reject(error); } - - await remakeRequest(error); - setSaveLoading(false); - authenticationErrorCount = 0; } } await clearBuildVerticesState(error); - return Promise.reject(error); + if ( + error?.response?.status !== 401 && + error?.response?.status !== 403 + ) { + return Promise.reject(error); + } }, ); @@ -141,21 +141,30 @@ function ApiInterceptor() { } async function tryToRenewAccessToken(error: AxiosError) { - try { - if (window.location.pathname.includes("/login")) return; - mutationRenewAccessToken({}); - } catch (error) { - clearBuildVerticesState(error); - mutationLogout(undefined, { - onSuccess: () => { - logout(); + if (window.location.pathname.includes("/login")) return; + mutationRenewAccessToken( + {}, + { + onSuccess: async (data) => { + authenticationErrorCount = 0; + await remakeRequest(error); + setSaveLoading(false); + authenticationErrorCount = 0; }, onError: (error) => { console.error(error); + mutationLogout(undefined, { + onSuccess: () => { + logout(); + }, + onError: (error) => { + console.error(error); + }, + }); + return Promise.reject("Authentication error"); }, - }); - return Promise.reject("Authentication error"); - } + }, + ); } async function clearBuildVerticesState(error) { From 3835f8d8b6ad0990734294ebceae7941c84cafd5 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa <72977554+Cristhianzl@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:23:51 -0300 Subject: [PATCH 11/14] refactor: change login/auto_login to use UseQuery (#3033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ (App.tsx): Introduce framer-motion library for animations in the frontend 🔧 (App.tsx): Update import path for useAutoLogin function in controllers/API/queries/auth 🔧 (index.ts): Remove autoLogin function from API controllers as it is now handled by useAutoLogin hook 🔧 (use-get-autologin.ts): Update function name to useAutoLogin and adjust return type 🔧 (use-post-login-user.ts): Update function name to loginUserFn for clarity 🔧 (LoginPage/index.tsx): Update import path for useLoginUser function in controllers/API/queries/auth 🔧 (LoginPage/index.tsx): Update function name to loginUserFn for clarity 🔧 (authStore.ts): Remove unused imports and functions related to autoLogin and login operations * 📝 (LoginPage/index.tsx): remove unnecessary console.log statement from onSuccess callback in mutate function * ♻️ (App.tsx): remove unused import statement for 'm' from 'framer-motion' to clean up code and improve readability * removing abort from request * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: anovazzi1 --- .../Basic Prompting (Hello, World).json | 4 +- .../starter_projects/Blog Writer.json | 8 +-- .../starter_projects/Complex Agent.json | 24 ++++++--- .../starter_projects/Document QA.json | 4 +- .../starter_projects/Hierarchical Agent.json | 12 +++-- .../starter_projects/Memory Chatbot.json | 4 +- .../starter_projects/Sequential Agent.json | 8 ++- .../starter_projects/Vector Store RAG.json | 44 ++++++++++++---- src/frontend/src/App.tsx | 23 ++++---- .../src/components/authGuard/index.tsx | 4 +- src/frontend/src/constants/constants.ts | 1 - src/frontend/src/contexts/authContext.tsx | 15 ++++-- src/frontend/src/controllers/API/index.ts | 52 ------------------- .../API/queries/auth/use-get-autologin.ts | 26 ++++++---- .../API/queries/auth/use-post-login-user.ts | 4 +- .../src/pages/AdminPage/LoginPage/index.tsx | 18 ++++--- src/frontend/src/pages/LoginPage/index.tsx | 21 +++++--- .../hooks/use-get-profile-pictures.ts | 25 --------- .../hooks/use-preload-images.tsx | 2 +- .../profilePictureChooserComponent/index.tsx | 2 +- .../components/ProfilePictureForm/index.tsx | 11 ++-- src/frontend/src/stores/authStore.ts | 1 - 22 files changed, 151 insertions(+), 162 deletions(-) delete mode 100644 src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-get-profile-pictures.ts diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json index c41428891730..8e5c0dad0ca2 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting (Hello, World).json @@ -678,7 +678,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json index a341db0f31c6..972a5c90aa5f 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json @@ -189,13 +189,13 @@ "show": true, "title_case": false, "type": "code", - "value": "import re\n\nfrom langchain_community.document_loaders.web_base import WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.io import MessageTextInput, Output\nfrom langflow.schema import Data\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, separated by commas.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"\n Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n raise ValueError(f\"Invalid URL: {string}\")\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n" + "value": "import re\n\nfrom langchain_community.document_loaders.web_base import WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.io import MessageTextInput, Output\nfrom langflow.schema import Data\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"\n Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n raise ValueError(f\"Invalid URL: {string}\")\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n" }, "urls": { "advanced": false, "display_name": "URLs", "dynamic": false, - "info": "Enter one or more URLs, separated by commas.", + "info": "Enter one or more URLs, by clicking the '+' button.", "input_types": [ "Message" ], @@ -866,7 +866,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json index a6ab3af9cf68..628ba2045b72 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json @@ -908,7 +908,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -2033,7 +2035,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -2731,7 +2735,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -3149,7 +3155,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -3592,7 +3600,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -4051,7 +4061,9 @@ "display_name": "SearchAPI API Key", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json b/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json index 94a094f9d356..02a8a5c9561d 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Document QA.json @@ -756,7 +756,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json index 3daf51e9e572..8a0e18ee230d 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json @@ -607,7 +607,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -1752,7 +1754,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -2635,7 +2639,9 @@ "display_name": "SearchAPI API Key", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json index 9641e99ab677..9c29c48fcbd4 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json @@ -536,7 +536,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json index ee1986dac491..b1a70afc6833 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json @@ -2054,7 +2054,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, @@ -2975,7 +2977,9 @@ "display_name": "SearchAPI API Key", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json index 399732cb9b58..d0bbe2ff2db4 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json @@ -569,7 +569,9 @@ "display_name": "API Endpoint", "dynamic": false, "info": "API endpoint URL for the Astra DB service.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_endpoint", "password": true, @@ -919,7 +921,9 @@ "display_name": "Astra DB Application Token", "dynamic": false, "info": "Authentication token for accessing Astra DB.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "token", "password": true, @@ -1779,7 +1783,9 @@ "display_name": "API Endpoint", "dynamic": false, "info": "API endpoint URL for the Astra DB service.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_endpoint", "password": true, @@ -2129,7 +2135,9 @@ "display_name": "Astra DB Application Token", "dynamic": false, "info": "Authentication token for accessing Astra DB.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "token", "password": true, @@ -2404,7 +2412,9 @@ "display_name": "OpenAI API Base", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "openai_api_base", "password": true, @@ -2420,7 +2430,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "openai_api_key", "password": true, @@ -2436,7 +2448,9 @@ "display_name": "OpenAI API Type", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "openai_api_type", "password": true, @@ -2851,7 +2865,9 @@ "display_name": "OpenAI API Base", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "openai_api_base", "password": true, @@ -2867,7 +2883,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "openai_api_key", "password": true, @@ -2883,7 +2901,9 @@ "display_name": "OpenAI API Type", "dynamic": false, "info": "", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "openai_api_type", "password": true, @@ -3119,7 +3139,9 @@ "display_name": "OpenAI API Key", "dynamic": false, "info": "The OpenAI API Key to use for the OpenAI model.", - "input_types": [], + "input_types": [ + "Message" + ], "load_from_db": true, "name": "api_key", "password": true, diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 3485d47bffde..e0038e2aed4b 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -14,7 +14,7 @@ import { LANGFLOW_AUTO_LOGIN_OPTION, } from "./constants/constants"; import { AuthContext } from "./contexts/authContext"; -import { autoLogin } from "./controllers/API"; +import { useAutoLogin } from "./controllers/API/queries/auth"; import { useGetHealthQuery } from "./controllers/API/queries/health"; import { useGetGlobalVariables } from "./controllers/API/queries/variables"; import { useGetVersionQuery } from "./controllers/API/queries/version"; @@ -38,6 +38,8 @@ export default function App() { const dark = useDarkStore((state) => state.dark); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const { mutate: mutateAutoLogin } = useAutoLogin(); + useGetVersionQuery(); const cookies = new Cookies(); @@ -61,11 +63,10 @@ export default function App() { }, [dark]); useEffect(() => { - const abortController = new AbortController(); const isLoginPage = location.pathname.includes("login"); - autoLogin(abortController.signal) - .then(async (user) => { + mutateAutoLogin(undefined, { + onSuccess: async (user) => { if (user && user["access_token"]) { user["refresh_token"] = "auto"; login(user["access_token"], "auto"); @@ -74,8 +75,8 @@ export default function App() { setAutoLogin(true); fetchAllData(); } - }) - .catch(async (error) => { + }, + onError: (error) => { if (error.name !== "CanceledError") { setAutoLogin(false); if ( @@ -94,14 +95,8 @@ export default function App() { useFlowsManagerStore.setState({ isLoading: false }); } } - }); - - /* - Abort the request as it isn't needed anymore, the component being - unmounted. It helps avoid, among other things, the well-known "can't - perform a React state update on an unmounted component" warning. - */ - return () => abortController.abort(); + }, + }); }, []); const fetchAllData = async () => { diff --git a/src/frontend/src/components/authGuard/index.tsx b/src/frontend/src/components/authGuard/index.tsx index 039894e5f768..8bfacbd6cdcb 100644 --- a/src/frontend/src/components/authGuard/index.tsx +++ b/src/frontend/src/components/authGuard/index.tsx @@ -1,3 +1,4 @@ +import { LANGFLOW_AUTO_LOGIN_OPTION } from "@/constants/constants"; import useAuthStore from "@/stores/authStore"; import { useContext } from "react"; import { AuthContext } from "../../contexts/authContext"; @@ -5,8 +6,9 @@ import { AuthContext } from "../../contexts/authContext"; export const ProtectedRoute = ({ children }) => { const { logout } = useContext(AuthContext); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const hasToken = !!localStorage.getItem(LANGFLOW_AUTO_LOGIN_OPTION); - if (!isAuthenticated) { + if (!isAuthenticated && hasToken) { logout(); } else { return children; diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 2a3859cbb3a3..a4d74462c143 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -1,7 +1,6 @@ // src/constants/constants.ts import { languageMap } from "../types/components"; -import { nodeNames } from "../utils/styleUtils"; /** * invalid characters for flow name diff --git a/src/frontend/src/contexts/authContext.tsx b/src/frontend/src/contexts/authContext.tsx index 907ca0fd65b6..735b912bb872 100644 --- a/src/frontend/src/contexts/authContext.tsx +++ b/src/frontend/src/contexts/authContext.tsx @@ -48,6 +48,10 @@ export function AuthProvider({ children }): React.ReactElement { const fetchApiData = useStoreStore((state) => state.fetchApiData); const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows); const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder); + const setIsAuthenticated = useAuthStore((state) => state.setIsAuthenticated); + const setIsAdmin = useAuthStore((state) => state.setIsAdmin); + const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading); + const autoLogin = useAuthStore((state) => state.autoLogin); useEffect(() => { const storedAccessToken = cookies.get(LANGFLOW_ACCESS_TOKEN); @@ -82,26 +86,27 @@ export function AuthProvider({ children }): React.ReactElement { function login(newAccessToken: string, autoLogin: string) { cookies.set(LANGFLOW_AUTO_LOGIN_OPTION, autoLogin, { path: "/" }); setAccessToken(newAccessToken); - useAuthStore.getState().setIsAuthenticated(true); + setIsAuthenticated(true); getUser(); } async function logout() { - if (useAuthStore.getState().autoLogin) { + if (autoLogin) { return; } try { await requestLogout(); cookies.remove(LANGFLOW_API_TOKEN, { path: "/" }); cookies.remove(LANGFLOW_AUTO_LOGIN_OPTION, { path: "/" }); - useAuthStore.getState().setIsAdmin(false); + setIsAdmin(false); setUserData(null); setAccessToken(null); - useAuthStore.getState().setIsAuthenticated(false); + setIsAuthenticated(false); + setIsAuthenticated(false); setAllFlows([]); setSelectedFolder(null); navigate("/login"); - useFlowsManagerStore.setState({ isLoading: false }); + setIsLoading(false); } catch (error) { console.error(error); throw error; diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 52ca5ba55a16..039f0a30c2dc 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -349,19 +349,6 @@ export async function uploadFile( return await api.post(`${BASE_URL_API}files/upload/${id}`, formData); } -export async function getProfilePictures(): Promise { - try { - const res = await api.get(`${BASE_URL_API}files/profile_pictures/list`); - - if (res.status === 200) { - return res.data; - } - } catch (error) { - throw error; - } - return null; -} - export async function postCustomComponent( code: string, apiClass: APIClassType, @@ -387,45 +374,6 @@ export async function postCustomComponentUpdate( }); } -export async function onLogin(user: LoginType) { - try { - const response = await api.post( - `${BASE_URL_API}login`, - new URLSearchParams({ - username: user.username, - password: user.password, - }).toString(), - { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }, - ); - - if (response.status === 200) { - const data = response?.data; - return data; - } - } catch (error) { - throw error; - } -} - -export async function autoLogin(abortSignal) { - try { - const response = await api.get(`${BASE_URL_API}auto_login`, { - signal: abortSignal, - }); - - if (response.status === 200) { - const data = response?.data; - return data; - } - } catch (error) { - throw error; - } -} - export async function renewAccessToken() { try { return await api.post(`${BASE_URL_API}refresh`); diff --git a/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts b/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts index f1f9a63bba42..a0531126858f 100644 --- a/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts +++ b/src/frontend/src/controllers/API/queries/auth/use-get-autologin.ts @@ -1,20 +1,24 @@ -import { keepPreviousData } from "@tanstack/react-query"; -import { Users, useQueryFunctionType } from "../../../../types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { useMutationFunctionType } from "../../../../types/api"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; -export const useGetAutoLogin: useQueryFunctionType = () => { - const { query } = UseRequestProcessor(); +export const useAutoLogin: useMutationFunctionType = ( + options?, +) => { + const { mutate } = UseRequestProcessor(); - const getIsAutoLogin = async () => { - const response = await api.get(`${getURL("AUTOLOGIN")}`); - return response["data"]; + const autoLoginFn = async (): Promise => { + const res = await api.get(`${getURL("AUTOLOGIN")}`); + return res.data; }; - const queryResult = query(["useGetAutoLogin"], getIsAutoLogin, { - placeholderData: keepPreviousData, - }); + const mutation: UseMutationResult = mutate( + ["useAutoLogin"], + autoLoginFn, + options, + ); - return queryResult; + return mutation; }; diff --git a/src/frontend/src/controllers/API/queries/auth/use-post-login-user.ts b/src/frontend/src/controllers/API/queries/auth/use-post-login-user.ts index 27c1ca5e2be7..1053a04af887 100644 --- a/src/frontend/src/controllers/API/queries/auth/use-post-login-user.ts +++ b/src/frontend/src/controllers/API/queries/auth/use-post-login-user.ts @@ -9,7 +9,7 @@ export const useLoginUser: useMutationFunctionType = ( ) => { const { mutate } = UseRequestProcessor(); - async function updateUser({ password, username }: LoginType): Promise { + async function loginUserFn({ password, username }: LoginType): Promise { const res = await api.post( `${getURL("LOGIN")}`, new URLSearchParams({ @@ -27,7 +27,7 @@ export const useLoginUser: useMutationFunctionType = ( const mutation: UseMutationResult = mutate( ["useLoginUser"], - updateUser, + loginUserFn, options, ); diff --git a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx index e4cccd574b8f..e0119ca6adc1 100644 --- a/src/frontend/src/pages/AdminPage/LoginPage/index.tsx +++ b/src/frontend/src/pages/AdminPage/LoginPage/index.tsx @@ -1,3 +1,4 @@ +import { useLoginUser } from "@/controllers/API/queries/auth"; import { useGetGlobalVariables } from "@/controllers/API/queries/variables"; import { useContext, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -6,7 +7,6 @@ import { Input } from "../../../components/ui/input"; import { SIGNIN_ERROR_ALERT } from "../../../constants/alerts_constants"; import { CONTROL_LOGIN_STATE } from "../../../constants/constants"; import { AuthContext } from "../../../contexts/authContext"; -import { onLogin } from "../../../controllers/API"; import useAlertStore from "../../../stores/alertStore"; import { LoginType } from "../../../types/api"; import { @@ -32,28 +32,30 @@ export default function LoginAdminPage() { setInputState((prev) => ({ ...prev, [name]: value })); } + const { mutate } = useLoginUser(); + function signIn() { const user: LoginType = { username: username, password: password, }; - onLogin(user) - .then((user) => { - console.log("admin page"); + mutate(user, { + onSuccess: (res) => { setLoading(true); - login(user.access_token, "login"); + login(res.access_token, "login"); mutateGetGlobalVariables(); navigate("/admin/"); - }) - .catch((error) => { + }, + onError: (error) => { setErrorData({ title: SIGNIN_ERROR_ALERT, list: [error["response"]["data"]["detail"]], }); - }); + }, + }); } return ( diff --git a/src/frontend/src/pages/LoginPage/index.tsx b/src/frontend/src/pages/LoginPage/index.tsx index ea2e0b553211..7c8fece043c8 100644 --- a/src/frontend/src/pages/LoginPage/index.tsx +++ b/src/frontend/src/pages/LoginPage/index.tsx @@ -1,3 +1,4 @@ +import { useLoginUser } from "@/controllers/API/queries/auth"; import * as Form from "@radix-ui/react-form"; import { useContext, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; @@ -7,7 +8,6 @@ import { Input } from "../../components/ui/input"; import { SIGNIN_ERROR_ALERT } from "../../constants/alerts_constants"; import { CONTROL_LOGIN_STATE } from "../../constants/constants"; import { AuthContext } from "../../contexts/authContext"; -import { onLogin } from "../../controllers/API"; import useAlertStore from "../../stores/alertStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { LoginType } from "../../types/api"; @@ -32,23 +32,28 @@ export default function LoginPage(): JSX.Element { setInputState((prev) => ({ ...prev, [name]: value })); } + const { mutate } = useLoginUser(); + function signIn() { const user: LoginType = { username: username.trim(), password: password.trim(), }; - onLogin(user) - .then((user) => { + + mutate(user, { + onSuccess: (data) => { + console.log("admin page"); setLoading(true); - login(user.access_token, "login"); - navigate("/"); - }) - .catch((error) => { + login(data.access_token, "login"); + navigate("/admin/"); + }, + onError: (error) => { setErrorData({ title: SIGNIN_ERROR_ALERT, list: [error["response"]["data"]["detail"]], }); - }); + }, + }); } return ( diff --git a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-get-profile-pictures.ts b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-get-profile-pictures.ts deleted file mode 100644 index 684fd5e38d72..000000000000 --- a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-get-profile-pictures.ts +++ /dev/null @@ -1,25 +0,0 @@ -import axios from "axios"; -import { PROFILE_PICTURES_GET_ERROR_ALERT } from "../../../../../../../../../constants/alerts_constants"; -import { getProfilePictures } from "../../../../../../../../../controllers/API"; - -const useGetProfilePictures = (setErrorData) => { - const handleGetProfilePictures = async () => { - try { - const profilePictures = await getProfilePictures(); - return profilePictures!.files; - } catch (error) { - if (axios.isCancel(error)) { - console.warn("Request canceled: ", error.message); - } else { - setErrorData({ - title: PROFILE_PICTURES_GET_ERROR_ALERT, - list: [(error as any)?.response?.data?.detail], - }); - } - } - }; - - return { handleGetProfilePictures }; -}; - -export default useGetProfilePictures; diff --git a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx index 020972b11fce..2b2c4a489745 100644 --- a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx @@ -2,9 +2,9 @@ import { useEffect } from "react"; import { BASE_URL_API } from "../../../../../../../../../constants/constants"; const usePreloadImages = ( - profilePictures?: { [key: string]: string[] }, setImagesLoaded: (value: boolean) => void, loading: boolean, + profilePictures?: { [key: string]: string[] }, ) => { const preloadImages = async (imageUrls) => { return Promise.all( diff --git a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx index ed5f2c2c2f0a..32a6057bd611 100644 --- a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/index.tsx @@ -30,7 +30,7 @@ export default function ProfilePictureChooserComponent({ } }, [ref, value]); - usePreloadImages(profilePictures, setImagesLoaded, loading); + usePreloadImages(setImagesLoaded, loading, profilePictures); return (
diff --git a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/index.tsx b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/index.tsx index cd800e2b3378..348e556d3add 100644 --- a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/index.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/index.tsx @@ -1,4 +1,7 @@ -import { ProfilePicturesQueryResponse } from "@/controllers/API/queries/files"; +import { + ProfilePicturesQueryResponse, + useGetProfilePicturesQuery, +} from "@/controllers/API/queries/files"; import * as Form from "@radix-ui/react-form"; import { UseQueryResult } from "@tanstack/react-query"; import { Button } from "../../../../../../components/ui/button"; @@ -27,7 +30,7 @@ const ProfilePictureFormComponent = ({ handleGetProfilePictures, userData, }: ProfilePictureFormComponentProps) => { - const { data: response, isFetching } = handleGetProfilePictures; + const { isLoading, data, isFetching } = useGetProfilePicturesQuery(); return (
((set, get) => ({ isAdmin: false, isAuthenticated: !!cookies.get(LANGFLOW_ACCESS_TOKEN), From 1c3ee13a856e701938d8db9d00512351f20df5c5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 19:11:46 -0300 Subject: [PATCH 12/14] refactor: improve file handling in loading starter projects (#3123) Refactor the `load_starter_projects` function in `setup.py` to improve error handling when loading starter projects. Instead of using a try-except block around the `orjson.loads` function, the function now uses a `with` statement to open the file and handle any JSON decoding errors. This ensures that any errors are properly caught and a `ValueError` is raised with the appropriate error message. Additionally, the function now logs a message when a starter project is successfully loaded. --- src/backend/base/langflow/initial_setup/setup.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index dcda6b50896d..dfcd1011a57c 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -343,12 +343,13 @@ def load_starter_projects() -> list[tuple[Path, dict]]: starter_projects = [] folder = Path(__file__).parent / "starter_projects" for file in folder.glob("*.json"): - try: - project = orjson.loads(file.read_text(encoding="utf-8")) - except orjson.JSONDecodeError as e: - raise ValueError(f"Error loading starter project {file}: {e}") - starter_projects.append((file, project)) - logger.info(f"Loaded starter project {file}") + with open(file, "r", encoding="utf-8") as f: + try: + project = orjson.loads(f.read()) + starter_projects.append((file, project)) + logger.info(f"Loaded starter project {file}") + except orjson.JSONDecodeError as e: + raise ValueError(f"Error loading starter project {file}: {e}") return starter_projects From 62191d92ae99163a364b672b17116bc82011da49 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 19:28:42 -0300 Subject: [PATCH 13/14] refactor: update CustomComponent constructor (#3114) * refactor: update code references to use _code instead of code * refactor: add backwards compatible attributes to Component class * refactor: update Component constructor to pass config params with underscore Refactored the `Component` class in `component.py` to handle inputs and outputs. Added a new method `map_outputs` to map a list of outputs to the component. Also updated the `__init__` method to properly initialize the inputs, outputs, and other attributes. This change improves the flexibility and extensibility of the `Component` class. Co-authored-by: Gabriel Luiz Freitas Almeida * refactor: change attribute to use underscore * refactor: update CustomComponent initialization parameters Refactored the `instantiate_class` function in `loading.py` to update the initialization parameters for the `CustomComponent` class. Changed the parameter names from `user_id`, `parameters`, `vertex`, and `tracing_service` to `_user_id`, `_parameters`, `_vertex`, and `_tracing_service` respectively. This change ensures consistency and improves code readability. Co-authored-by: Gabriel Luiz Freitas Almeida * refactor: update BaseComponent to accept UUID for _user_id Updated the `BaseComponent` class in `base_component.py` to accept a `UUID` type for the `_user_id` attribute. This change improves the type safety and ensures consistency with the usage of `_user_id` throughout the codebase. * refactor: import nanoid with type annotation The `nanoid` import in `component.py` has been updated to include a type annotation `# type: ignore`. This change ensures that the type checker ignores any errors related to the `nanoid` import. * fix(custom_component.py): convert _user_id to string before passing to functions to ensure compatibility with function signatures * refactor(utils.py): refactor code to use _user_id instead of user_id for consistency and clarity perf(utils.py): optimize code by reusing cc_instance instead of calling get_component_instance multiple times * [autofix.ci] apply automated fixes * refactor: update BaseComponent to use get_template_config method Refactored the `BaseComponent` class in `base_component.py` to use the `get_template_config` method instead of duplicating the code. This change improves code readability and reduces redundancy. * refactor: update build_custom_component_template to use add_name instead of keep_name Refactor the `build_custom_component_template` function in `utils.py` to use the `add_name` parameter instead of the deprecated `keep_name` parameter. This change ensures consistency with the updated method signature and improves code clarity. * feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components (#3115) * feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components * refactor: extract method to get method return type in CustomComponent * refactor: update _extract_return_type method in CustomComponent to accept Any type The _extract_return_type method in CustomComponent has been updated to accept the Any type as the return_type parameter. This change improves the flexibility and compatibility of the method, allowing it to handle a wider range of return types. * refactor: add _template_config property to BaseComponent Add a new `_template_config` property to the `BaseComponent` class in `base_component.py`. This property is used to store the template configuration for the custom component. If the `_template_config` property is empty, it is populated by calling the `build_template_config` method. This change improves the efficiency of accessing the template configuration and ensures that it is only built when needed. * refactor: add type checking for Output types in add_types method Improve type checking in the `add_types` method of the `Output` class in `base.py`. Check if the `type_` already exists in the `types` list before adding it. This change ensures that duplicate types are not added to the list. * update starter projects * refactor: optimize imports in base.py Optimize imports in the `base.py` file by removing unused imports and organizing the remaining imports. This change improves code readability and reduces unnecessary clutter. * fix(base.py): fix condition to check if self.types is not None before checking if type_ is in self.types --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../base/langflow/base/agents/crewai/crew.py | 6 +- .../base/langflow/base/models/model.py | 6 +- .../langflow/components/prototypes/Listen.py | 6 +- .../langflow/components/prototypes/Notify.py | 6 +- .../custom/custom_component/base_component.py | 38 +++++--- .../custom/custom_component/component.py | 90 ++++++++++++++++--- .../custom_component/custom_component.py | 69 +++++++------- src/backend/base/langflow/custom/utils.py | 13 +-- .../starter_projects/Complex Agent.json | 6 +- .../starter_projects/Hierarchical Agent.json | 6 +- .../starter_projects/Sequential Agent.json | 6 +- .../langflow/interface/initialize/loading.py | 16 ++-- .../base/langflow/services/tracing/service.py | 6 +- .../base/langflow/template/field/base.py | 7 +- 14 files changed, 180 insertions(+), 101 deletions(-) diff --git a/src/backend/base/langflow/base/agents/crewai/crew.py b/src/backend/base/langflow/base/agents/crewai/crew.py index 8326c3965ef0..359b87591fd4 100644 --- a/src/backend/base/langflow/base/agents/crewai/crew.py +++ b/src/backend/base/langflow/base/agents/crewai/crew.py @@ -51,8 +51,8 @@ def get_task_callback( self, ) -> Callable: def task_callback(task_output: TaskOutput): - if self.vertex: - vertex_id = self.vertex.id + if self._vertex: + vertex_id = self._vertex.id else: vertex_id = self.display_name or self.__class__.__name__ self.log(task_output.model_dump(), name=f"Task (Agent: {task_output.agent}) - {vertex_id}") @@ -63,7 +63,7 @@ def get_step_callback( self, ) -> Callable: def step_callback(agent_output: Union[AgentFinish, List[Tuple[AgentAction, str]]]): - _id = self.vertex.id if self.vertex else self.display_name + _id = self._vertex.id if self._vertex else self.display_name if isinstance(agent_output, AgentFinish): messages = agent_output.messages self.log(cast(dict, messages[0].to_json()), name=f"Finish (Agent: {_id})") diff --git a/src/backend/base/langflow/base/models/model.py b/src/backend/base/langflow/base/models/model.py index 6a2aeda1d534..9a1057c62768 100644 --- a/src/backend/base/langflow/base/models/model.py +++ b/src/backend/base/langflow/base/models/model.py @@ -1,7 +1,7 @@ import json import warnings from abc import abstractmethod -from typing import Optional, Union, List +from typing import List, Optional, Union from langchain_core.language_models.llms import LLM from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage @@ -10,7 +10,7 @@ from langflow.custom import Component from langflow.field_typing import LanguageModel from langflow.inputs import MessageInput, MessageTextInput -from langflow.inputs.inputs import InputTypes, BoolInput +from langflow.inputs.inputs import BoolInput, InputTypes from langflow.schema.message import Message from langflow.template.field.base import Output @@ -164,7 +164,7 @@ def get_chat_result( inputs: Union[list, dict] = messages or {} try: runnable = runnable.with_config( # type: ignore - {"run_name": self.display_name, "project_name": self.tracing_service.project_name} # type: ignore + {"run_name": self.display_name, "project_name": self._tracing_service.project_name} # type: ignore ) if stream: return runnable.stream(inputs) # type: ignore diff --git a/src/backend/base/langflow/components/prototypes/Listen.py b/src/backend/base/langflow/components/prototypes/Listen.py index 6e5de723a87a..e75ec070b8ac 100644 --- a/src/backend/base/langflow/components/prototypes/Listen.py +++ b/src/backend/base/langflow/components/prototypes/Listen.py @@ -23,6 +23,6 @@ def build(self, name: str) -> Data: return state def _set_successors_ids(self): - self.vertex.is_state = True - successors = self.vertex.graph.successor_map.get(self.vertex.id, []) - return successors + self.vertex.graph.activated_vertices + self._vertex.is_state = True + successors = self._vertex.graph.successor_map.get(self._vertex.id, []) + return successors + self._vertex.graph.activated_vertices diff --git a/src/backend/base/langflow/components/prototypes/Notify.py b/src/backend/base/langflow/components/prototypes/Notify.py index b83331e3a914..72287a25509b 100644 --- a/src/backend/base/langflow/components/prototypes/Notify.py +++ b/src/backend/base/langflow/components/prototypes/Notify.py @@ -43,6 +43,6 @@ def build(self, name: str, data: Optional[Data] = None, append: bool = False) -> return data def _set_successors_ids(self): - self.vertex.is_state = True - successors = self.vertex.graph.successor_map.get(self.vertex.id, []) - return successors + self.vertex.graph.activated_vertices + self._vertex.is_state = True + successors = self._vertex.graph.successor_map.get(self._vertex.id, []) + return successors + self._vertex.graph.activated_vertices diff --git a/src/backend/base/langflow/custom/custom_component/base_component.py b/src/backend/base/langflow/custom/custom_component/base_component.py index ce1c5c445b44..58d4daa46223 100644 --- a/src/backend/base/langflow/custom/custom_component/base_component.py +++ b/src/backend/base/langflow/custom/custom_component/base_component.py @@ -1,6 +1,7 @@ import operator import warnings from typing import Any, ClassVar, Optional +from uuid import UUID from cachetools import TTLCache, cachedmethod from fastapi import HTTPException @@ -27,7 +28,8 @@ class BaseComponent: """The code of the component. Defaults to None.""" _function_entrypoint_name: str = "build" field_config: dict = {} - _user_id: Optional[str] + _user_id: Optional[str | UUID] = None + _template_config: dict = {} def __init__(self, **data): self.cache = TTLCache(maxsize=1024, ttl=60) @@ -38,7 +40,7 @@ def __init__(self, **data): setattr(self, key, value) def __setattr__(self, key, value): - if key == "_user_id" and hasattr(self, "_user_id"): + if key == "_user_id" and hasattr(self, "_user_id") and getattr(self, "_user_id") is not None: warnings.warn("user_id is immutable and cannot be changed.") super().__setattr__(key, value) @@ -65,23 +67,16 @@ def get_function(self): return validate.create_function(self._code, self._function_entrypoint_name) - def build_template_config(self) -> dict: + @staticmethod + def get_template_config(component): """ - Builds the template configuration for the custom component. - - Returns: - A dictionary representing the template configuration. + Gets the template configuration for the custom component itself. """ - if not self._code: - return {} - - cc_class = eval_custom_component_code(self._code) - component_instance = cc_class() template_config = {} for attribute, func in ATTR_FUNC_MAPPING.items(): - if hasattr(component_instance, attribute): - value = getattr(component_instance, attribute) + if hasattr(component, attribute): + value = getattr(component, attribute) if value is not None: template_config[attribute] = func(value=value) @@ -91,5 +86,20 @@ def build_template_config(self) -> dict: return template_config + def build_template_config(self) -> dict: + """ + Builds the template configuration for the custom component. + + Returns: + A dictionary representing the template configuration. + """ + if not self._code: + return {} + + cc_class = eval_custom_component_code(self._code) + component_instance = cc_class() + template_config = self.get_template_config(component_instance) + return template_config + def build(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError diff --git a/src/backend/base/langflow/custom/custom_component/component.py b/src/backend/base/langflow/custom/custom_component/component.py index 2aafd92109ce..a5373b449fdc 100644 --- a/src/backend/base/langflow/custom/custom_component/component.py +++ b/src/backend/base/langflow/custom/custom_component/component.py @@ -1,10 +1,12 @@ import inspect -from typing import Any, Callable, ClassVar, List, Optional, Union +from typing import Any, Callable, ClassVar, List, Optional, Union, get_type_hints from uuid import UUID +import nanoid # type: ignore import yaml from pydantic import BaseModel +from langflow.helpers.custom import format_type from langflow.inputs.inputs import InputTypes from langflow.schema.artifact import get_artifact_type, post_process_raw from langflow.schema.data import Data @@ -14,6 +16,8 @@ from .custom_component import CustomComponent +BACKWARDS_COMPATIBLE_ATTRIBUTES = ["user_id", "vertex", "tracing_service"] + class Component(CustomComponent): inputs: List[InputTypes] = [] @@ -21,24 +25,45 @@ class Component(CustomComponent): code_class_base_inheritance: ClassVar[str] = "Component" _output_logs: dict[str, Log] = {} - def __init__(self, **data): + def __init__(self, **kwargs): + # if key starts with _ it is a config + # else it is an input + inputs = {} + config = {} + for key, value in kwargs.items(): + if key.startswith("_"): + config[key] = value + else: + inputs[key] = value self._inputs: dict[str, InputTypes] = {} + self._outputs: dict[str, Output] = {} self._results: dict[str, Any] = {} self._attributes: dict[str, Any] = {} - self._parameters: dict[str, Any] = {} + self._parameters = inputs or {} + self._components: list[Component] = [] + self.set_attributes(self._parameters) self._output_logs = {} - super().__init__(**data) + config = config or {} + if "_id" not in config: + config |= {"_id": f"{self.__class__.__name__}-{nanoid.generate(size=5)}"} + super().__init__(**config) + if hasattr(self, "_trace_type"): + self.trace_type = self._trace_type if not hasattr(self, "trace_type"): self.trace_type = "chain" if self.inputs is not None: self.map_inputs(self.inputs) - self.set_attributes(self._parameters) + if self.outputs is not None: + self.map_outputs(self.outputs) + self._set_output_types() def __getattr__(self, name: str) -> Any: if "_attributes" in self.__dict__ and name in self.__dict__["_attributes"]: return self.__dict__["_attributes"][name] if "_inputs" in self.__dict__ and name in self.__dict__["_inputs"]: return self.__dict__["_inputs"][name].value + if name in BACKWARDS_COMPATIBLE_ATTRIBUTES: + return self.__dict__[f"_{name}"] raise AttributeError(f"{name} not found in {self.__class__.__name__}") def map_inputs(self, inputs: List[InputTypes]): @@ -48,10 +73,47 @@ def map_inputs(self, inputs: List[InputTypes]): raise ValueError("Input name cannot be None.") self._inputs[input_.name] = input_ + def map_outputs(self, outputs: List[Output]): + """ + Maps the given list of outputs to the component. + Args: + outputs (List[Output]): The list of outputs to be mapped. + Raises: + ValueError: If the output name is None. + Returns: + None + """ + self.outputs = outputs + for output in outputs: + if output.name is None: + raise ValueError("Output name cannot be None.") + self._outputs[output.name] = output + def validate(self, params: dict): self._validate_inputs(params) self._validate_outputs() + def _set_output_types(self): + for output in self.outputs: + return_types = self._get_method_return_type(output.method) + output.add_types(return_types) + output.set_selected() + + def _get_method_return_type(self, method_name: str) -> List[str]: + method = getattr(self, method_name) + return_type = get_type_hints(method)["return"] + extracted_return_types = self._extract_return_type(return_type) + return [format_type(extracted_return_type) for extracted_return_type in extracted_return_types] + + def _get_output_by_method(self, method: Callable): + # method is a callable and output.method is a string + # we need to find the output that has the same method + output = next((output for output in self.outputs if output.method == method.__name__), None) + if output is None: + method_name = method.__name__ if hasattr(method, "__name__") else str(method) + raise ValueError(f"Output with method {method_name} not found") + return output + def _validate_outputs(self): # Raise Error if some rule isn't met pass @@ -106,9 +168,9 @@ def get_trace_as_metadata(self): async def _build_with_tracing(self): inputs = self.get_trace_as_inputs() metadata = self.get_trace_as_metadata() - async with self.tracing_service.trace_context(self, self.trace_name, inputs, metadata): + async with self._tracing_service.trace_context(self, self.trace_name, inputs, metadata): _results, _artifacts = await self._build_results() - self.tracing_service.set_outputs(self.trace_name, _results) + self._tracing_service.set_outputs(self.trace_name, _results) return _results, _artifacts @@ -116,7 +178,7 @@ async def _build_without_tracing(self): return await self._build_results() async def build_results(self): - if self.tracing_service: + if self._tracing_service: return await self._build_with_tracing() return await self._build_without_tracing() @@ -124,11 +186,11 @@ async def _build_results(self): _results = {} _artifacts = {} if hasattr(self, "outputs"): - self._set_outputs(self.vertex.outputs) + self._set_outputs(self._vertex.outputs) for output in self.outputs: # Build the output if it's connected to some other vertex # or if it's not connected to any vertex - if not self.vertex.outgoing_edges or output.name in self.vertex.edges_source_names: + if not self._vertex.outgoing_edges or output.name in self._vertex.edges_source_names: if output.method is None: raise ValueError(f"Output {output.name} does not have a method defined.") method: Callable = getattr(self, output.method) @@ -142,9 +204,9 @@ async def _build_results(self): if ( isinstance(result, Message) and result.flow_id is None - and self.vertex.graph.flow_id is not None + and self._vertex.graph.flow_id is not None ): - result.set_flow_id(self.vertex.graph.flow_id) + result.set_flow_id(self._vertex.graph.flow_id) _results[output.name] = result output.value = result custom_repr = self.custom_repr() @@ -176,8 +238,8 @@ async def _build_results(self): self._logs = [] self._artifacts = _artifacts self._results = _results - if self.tracing_service: - self.tracing_service.set_outputs(self.trace_name, _results) + if self._tracing_service: + self._tracing_service.set_outputs(self.trace_name, _results) return _results, _artifacts def custom_repr(self): diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index 95c10022f033..d99fd6561ba1 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -1,6 +1,5 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar, List, Optional, Sequence, Union -from uuid import UUID import yaml from cachetools import TTLCache @@ -14,7 +13,7 @@ from langflow.schema.dotdict import dotdict from langflow.schema.log import LoggableType from langflow.schema.schema import OutputValue -from langflow.services.deps import get_storage_service, get_tracing_service, get_variable_service, session_scope +from langflow.services.deps import get_storage_service, get_variable_service, session_scope from langflow.services.storage.service import StorageService from langflow.services.tracing.schema import Log from langflow.template.utils import update_frontend_node_with_template_values @@ -72,20 +71,19 @@ class CustomComponent(BaseComponent): """The default frozen state of the component. Defaults to False.""" build_parameters: Optional[dict] = None """The build parameters of the component. Defaults to None.""" - vertex: Optional["Vertex"] = None + _vertex: Optional["Vertex"] = None """The edge target parameter of the component. Defaults to None.""" code_class_base_inheritance: ClassVar[str] = "CustomComponent" function_entrypoint_name: ClassVar[str] = "build" function: Optional[Callable] = None repr_value: Optional[Any] = "" - user_id: Optional[Union[UUID, str]] = None status: Optional[Any] = None """The status of the component. This is displayed on the frontend. Defaults to None.""" _flows_data: Optional[List[Data]] = None _outputs: List[OutputValue] = [] _logs: List[Log] = [] _output_logs: dict[str, Log] = {} - tracing_service: Optional["TracingService"] = None + _tracing_service: Optional["TracingService"] = None def set_attributes(self, parameters: dict): pass @@ -94,51 +92,43 @@ def set_parameters(self, parameters: dict): self._parameters = parameters self.set_attributes(self._parameters) - @classmethod - def initialize(cls, **kwargs): - user_id = kwargs.pop("user_id", None) - vertex = kwargs.pop("vertex", None) - tracing_service = kwargs.pop("tracing_service", get_tracing_service()) - params_copy = kwargs.copy() - return cls(user_id=user_id, _parameters=params_copy, vertex=vertex, tracing_service=tracing_service) - @property def trace_name(self): - return f"{self.display_name} ({self.vertex.id})" + return f"{self.display_name} ({self._vertex.id})" def update_state(self, name: str, value: Any): - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id) + self._vertex.graph.update_state(name=name, record=value, caller=self._vertex.id) except Exception as e: raise ValueError(f"Error updating state: {e}") def stop(self, output_name: str | None = None): - if not output_name and self.vertex and len(self.vertex.outputs) == 1: - output_name = self.vertex.outputs[0]["name"] + if not output_name and self._vertex and len(self._vertex.outputs) == 1: + output_name = self._vertex.outputs[0]["name"] elif not output_name: raise ValueError("You must specify an output name to call stop") - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - self.graph.mark_branch(vertex_id=self.vertex.id, output_name=output_name, state="INACTIVE") + self.graph.mark_branch(vertex_id=self._vertex.id, output_name=output_name, state="INACTIVE") except Exception as e: raise ValueError(f"Error stopping {self.display_name}: {e}") def append_state(self, name: str, value: Any): - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id) + self._vertex.graph.append_state(name=name, record=value, caller=self._vertex.id) except Exception as e: raise ValueError(f"Error appending state: {e}") def get_state(self, name: str): - if not self.vertex: + if not self._vertex: raise ValueError("Vertex is not set") try: - return self.vertex.graph.get_state(name=name) + return self._vertex.graph.get_state(name=name) except Exception as e: raise ValueError(f"Error getting state: {e}") @@ -176,7 +166,7 @@ def get_full_path(self, path: str) -> str: @property def graph(self): - return self.vertex.graph + return self._vertex.graph def _get_field_order(self): return self.field_order or list(self.field_config.keys()) @@ -277,6 +267,14 @@ def to_data(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bo return data_objects + def get_method_return_type(self, method_name: str): + build_method = self.get_method(method_name) + if not build_method or not build_method.get("has_return"): + return [] + return_type = build_method["return_type"] + + return self._extract_return_type(return_type) + def create_references_from_data(self, data: List[Data], include_data: bool = False) -> str: """ Create references from a list of data. @@ -349,12 +347,7 @@ def get_function_entrypoint_return_type(self) -> List[Any]: """ return self.get_method_return_type(self.function_entrypoint_name) - def get_method_return_type(self, method_name: str): - build_method = self.get_method(method_name) - if not build_method or not build_method.get("has_return"): - return [] - return_type = build_method["return_type"] - + def _extract_return_type(self, return_type: Any): if hasattr(return_type, "__origin__") and return_type.__origin__ in [ list, List, @@ -401,7 +394,9 @@ def template_config(self): Returns: dict: The template configuration for the custom component. """ - return self.build_template_config() + if not self._template_config: + self._template_config = self.build_template_config() + return self._template_config @property def variables(self): @@ -471,7 +466,7 @@ def get_function(self): async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> "Graph": if not self._user_id: raise ValueError("Session is invalid") - return await load_flow(user_id=self._user_id, flow_id=flow_id, tweaks=tweaks) + return await load_flow(user_id=str(self._user_id), flow_id=flow_id, tweaks=tweaks) async def run_flow( self, @@ -487,14 +482,14 @@ async def run_flow( flow_id=flow_id, flow_name=flow_name, tweaks=tweaks, - user_id=self._user_id, + user_id=str(self._user_id), ) def list_flows(self) -> List[Data]: if not self._user_id: raise ValueError("Session is invalid") try: - return list_flows(user_id=self._user_id) + return list_flows(user_id=str(self._user_id)) except Exception as e: raise ValueError(f"Error listing flows: {e}") @@ -522,8 +517,8 @@ def log(self, message: LoggableType | list[LoggableType], name: Optional[str] = name = f"Log {len(self._logs) + 1}" log = Log(message=message, type=get_artifact_type(message), name=name) self._logs.append(log) - if self.tracing_service and self.vertex: - self.tracing_service.add_log(trace_name=self.trace_name, log=log) + if self._tracing_service and self._vertex: + self._tracing_service.add_log(trace_name=self.trace_name, log=log) def post_code_processing(self, new_frontend_node: dict, current_frontend_node: dict): """ diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index 84aef23994ff..5d8a9c664b37 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -283,7 +283,7 @@ def get_component_instance(custom_component: CustomComponent, user_id: Optional[ ) from exc try: - custom_instance = custom_class(user_id=user_id) + custom_instance = custom_class(_user_id=user_id) return custom_instance except Exception as exc: logger.error(f"Error while instantiating custom component: {str(exc)}") @@ -317,7 +317,7 @@ def run_build_config( ) from exc try: - custom_instance = custom_class(user_id=user_id) + custom_instance = custom_class(_user_id=user_id) build_config: Dict = custom_instance.build_config() for field_name, field in build_config.copy().items(): @@ -361,14 +361,15 @@ def build_custom_component_template_from_inputs( custom_component: Union[Component, CustomComponent], user_id: Optional[Union[str, UUID]] = None ): # The List of Inputs fills the role of the build_config and the entrypoint_args - field_config = custom_component.template_config + cc_instance = get_component_instance(custom_component, user_id=user_id) + field_config = cc_instance.get_template_config(cc_instance) frontend_node = ComponentFrontendNode.from_inputs(**field_config) frontend_node = add_code_field(frontend_node, custom_component._code, field_config.get("code", {})) # But we now need to calculate the return_type of the methods in the outputs for output in frontend_node.outputs: if output.types: continue - return_types = custom_component.get_method_return_type(output.method) + return_types = cc_instance.get_method_return_type(output.method) return_types = [format_type(return_type) for return_type in return_types] output.add_types(return_types) output.set_selected() @@ -376,8 +377,8 @@ def build_custom_component_template_from_inputs( frontend_node.validate_component() # ! This should be removed when we have a better way to handle this frontend_node.set_base_classes_from_outputs() - reorder_fields(frontend_node, custom_component._get_field_order()) - cc_instance = get_component_instance(custom_component, user_id=user_id) + reorder_fields(frontend_node, cc_instance._get_field_order()) + return frontend_node.to_dict(add_name=False), cc_instance diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json index 628ba2045b72..08d6086151dd 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Complex Agent.json @@ -4037,7 +4037,8 @@ "name": "api_run_model", "selected": "Data", "types": [ - "Data" + "Data", + "list" ], "value": "__UNDEFINED__" }, @@ -4048,7 +4049,8 @@ "name": "api_build_tool", "selected": "Tool", "types": [ - "Tool" + "Tool", + "Sequence" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json index 8a0e18ee230d..7a48edba9aeb 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Hierarchical Agent.json @@ -2615,7 +2615,8 @@ "name": "api_run_model", "selected": "Data", "types": [ - "Data" + "Data", + "list" ], "value": "__UNDEFINED__" }, @@ -2626,7 +2627,8 @@ "name": "api_build_tool", "selected": "Tool", "types": [ - "Tool" + "Tool", + "Sequence" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json index b1a70afc6833..5364b52fd015 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Agent.json @@ -2953,7 +2953,8 @@ "name": "api_run_model", "selected": "Data", "types": [ - "Data" + "Data", + "list" ], "value": "__UNDEFINED__" }, @@ -2964,7 +2965,8 @@ "name": "api_build_tool", "selected": "Tool", "types": [ - "Tool" + "Tool", + "Sequence" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/interface/initialize/loading.py b/src/backend/base/langflow/interface/initialize/loading.py index f090febf3a8a..ea2858100ac4 100644 --- a/src/backend/base/langflow/interface/initialize/loading.py +++ b/src/backend/base/langflow/interface/initialize/loading.py @@ -33,11 +33,11 @@ async def instantiate_class( custom_params = get_params(vertex.params) code = custom_params.pop("code") class_object: Type["CustomComponent" | "Component"] = eval_custom_component_code(code) - custom_component: "CustomComponent" | "Component" = class_object.initialize( - user_id=user_id, - parameters=custom_params, - vertex=vertex, - tracing_service=get_tracing_service(), + custom_component: "CustomComponent" | "Component" = class_object( + _user_id=user_id, + _parameters=custom_params, + _vertex=vertex, + _tracing_service=get_tracing_service(), ) return custom_component, custom_params @@ -186,9 +186,9 @@ async def build_custom_component(params: dict, custom_component: "CustomComponen raw = post_process_raw(raw, artifact_type) artifact = {"repr": custom_repr, "raw": raw, "type": artifact_type} - if custom_component.vertex is not None: - custom_component._artifacts = {custom_component.vertex.outputs[0].get("name"): artifact} - custom_component._results = {custom_component.vertex.outputs[0].get("name"): build_result} + if custom_component._vertex is not None: + custom_component._artifacts = {custom_component._vertex.outputs[0].get("name"): artifact} + custom_component._results = {custom_component._vertex.outputs[0].get("name"): build_result} return custom_component, build_result, artifact raise ValueError("Custom component does not have a vertex") diff --git a/src/backend/base/langflow/services/tracing/service.py b/src/backend/base/langflow/services/tracing/service.py index feb74e3a2344..6a3b7ac987c3 100644 --- a/src/backend/base/langflow/services/tracing/service.py +++ b/src/backend/base/langflow/services/tracing/service.py @@ -194,8 +194,8 @@ async def trace_context( metadata: Optional[Dict[str, Any]] = None, ): trace_id = trace_name - if component.vertex: - trace_id = component.vertex.id + if component._vertex: + trace_id = component._vertex.id trace_type = component.trace_type self._start_traces( trace_id, @@ -203,7 +203,7 @@ async def trace_context( trace_type, self._cleanup_inputs(inputs), metadata, - component.vertex, + component._vertex, ) try: yield self diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index e825762301cc..79ca2d3d9a63 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -1,5 +1,8 @@ from enum import Enum -from typing import Any, Callable, GenericAlias, Optional, Union, _GenericAlias, _UnionGenericAlias # type: ignore +from typing import GenericAlias # type: ignore +from typing import _GenericAlias # type: ignore +from typing import _UnionGenericAlias # type: ignore +from typing import Any, Callable, Optional, Union from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer, model_validator @@ -182,6 +185,8 @@ def to_dict(self): def add_types(self, _type: list[Any]): for type_ in _type: + if self.types and type_ in self.types: + continue if self.types is None: self.types = [] self.types.append(type_) From 34bb9d91301c2292ca12d53ec297ef106a30dbf0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 31 Jul 2024 20:44:02 -0300 Subject: [PATCH 14/14] refactor: parameter name 'add_name' to 'keep_name' (#3116) * refactor: update code references to use _code instead of code * refactor: add backwards compatible attributes to Component class * refactor: update Component constructor to pass config params with underscore Refactored the `Component` class in `component.py` to handle inputs and outputs. Added a new method `map_outputs` to map a list of outputs to the component. Also updated the `__init__` method to properly initialize the inputs, outputs, and other attributes. This change improves the flexibility and extensibility of the `Component` class. Co-authored-by: Gabriel Luiz Freitas Almeida * refactor: change attribute to use underscore * refactor: update CustomComponent initialization parameters Refactored the `instantiate_class` function in `loading.py` to update the initialization parameters for the `CustomComponent` class. Changed the parameter names from `user_id`, `parameters`, `vertex`, and `tracing_service` to `_user_id`, `_parameters`, `_vertex`, and `_tracing_service` respectively. This change ensures consistency and improves code readability. Co-authored-by: Gabriel Luiz Freitas Almeida * refactor: update BaseComponent to accept UUID for _user_id Updated the `BaseComponent` class in `base_component.py` to accept a `UUID` type for the `_user_id` attribute. This change improves the type safety and ensures consistency with the usage of `_user_id` throughout the codebase. * refactor: import nanoid with type annotation The `nanoid` import in `component.py` has been updated to include a type annotation `# type: ignore`. This change ensures that the type checker ignores any errors related to the `nanoid` import. * fix(custom_component.py): convert _user_id to string before passing to functions to ensure compatibility with function signatures * feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components * refactor: extract method to get method return type in CustomComponent * refactor(utils.py): refactor code to use _user_id instead of user_id for consistency and clarity perf(utils.py): optimize code by reusing cc_instance instead of calling get_component_instance multiple times * refactor(utils.py, base.py): change parameter name 'add_name' to 'keep_name' for clarity and consistency in codebase * [autofix.ci] apply automated fixes * refactor: update _extract_return_type method in CustomComponent to accept Any type The _extract_return_type method in CustomComponent has been updated to accept the Any type as the return_type parameter. This change improves the flexibility and compatibility of the method, allowing it to handle a wider range of return types. * refactor: update BaseComponent to use get_template_config method Refactored the `BaseComponent` class in `base_component.py` to use the `get_template_config` method instead of duplicating the code. This change improves code readability and reduces redundancy. * refactor: update build_custom_component_template to use add_name instead of keep_name Refactor the `build_custom_component_template` function in `utils.py` to use the `add_name` parameter instead of the deprecated `keep_name` parameter. This change ensures consistency with the updated method signature and improves code clarity. * feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components (#3115) * feat(component.py): add method to set output types based on method return type to improve type checking and validation in custom components * refactor: extract method to get method return type in CustomComponent * refactor: update _extract_return_type method in CustomComponent to accept Any type The _extract_return_type method in CustomComponent has been updated to accept the Any type as the return_type parameter. This change improves the flexibility and compatibility of the method, allowing it to handle a wider range of return types. * refactor: add _template_config property to BaseComponent Add a new `_template_config` property to the `BaseComponent` class in `base_component.py`. This property is used to store the template configuration for the custom component. If the `_template_config` property is empty, it is populated by calling the `build_template_config` method. This change improves the efficiency of accessing the template configuration and ensures that it is only built when needed. * refactor: add type checking for Output types in add_types method Improve type checking in the `add_types` method of the `Output` class in `base.py`. Check if the `type_` already exists in the `types` list before adding it. This change ensures that duplicate types are not added to the list. * update starter projects * refactor: optimize imports in base.py Optimize imports in the `base.py` file by removing unused imports and organizing the remaining imports. This change improves code readability and reduces unnecessary clutter. * fix(base.py): fix condition to check if self.types is not None before checking if type_ is in self.types * refactor: update build_custom_component_template to use add_name instead of keep_name --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/base/langflow/custom/utils.py | 8 ++++---- src/backend/base/langflow/template/frontend_node/base.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/base/langflow/custom/utils.py b/src/backend/base/langflow/custom/utils.py index 5d8a9c664b37..48beb3de7a02 100644 --- a/src/backend/base/langflow/custom/utils.py +++ b/src/backend/base/langflow/custom/utils.py @@ -377,9 +377,9 @@ def build_custom_component_template_from_inputs( frontend_node.validate_component() # ! This should be removed when we have a better way to handle this frontend_node.set_base_classes_from_outputs() - reorder_fields(frontend_node, cc_instance._get_field_order()) - - return frontend_node.to_dict(add_name=False), cc_instance + reorder_fields(frontend_node, custom_component._get_field_order()) + cc_instance = get_component_instance(custom_component, user_id=user_id) + return frontend_node.to_dict(keep_name=False), cc_instance def build_custom_component_template( @@ -415,7 +415,7 @@ def build_custom_component_template( reorder_fields(frontend_node, custom_instance._get_field_order()) - return frontend_node.to_dict(add_name=False), custom_instance + return frontend_node.to_dict(keep_name=False), custom_instance except Exception as exc: if isinstance(exc, HTTPException): raise exc diff --git a/src/backend/base/langflow/template/frontend_node/base.py b/src/backend/base/langflow/template/frontend_node/base.py index 77b6d1f69ae5..9aeb0b97dd9b 100644 --- a/src/backend/base/langflow/template/frontend_node/base.py +++ b/src/backend/base/langflow/template/frontend_node/base.py @@ -90,10 +90,10 @@ def serialize_model(self, handler): return {name: result} # For backwards compatibility - def to_dict(self, add_name=True) -> dict: + def to_dict(self, keep_name=True) -> dict: """Returns a dict representation of the frontend node.""" dump = self.model_dump(by_alias=True, exclude_none=True) - if not add_name: + if not keep_name: return dump.pop(self.name) return dump