diff --git a/src/backend/base/langflow/components/data/APIRequest.py b/src/backend/base/langflow/components/data/APIRequest.py index 1731244a3..969031072 100644 --- a/src/backend/base/langflow/components/data/APIRequest.py +++ b/src/backend/base/langflow/components/data/APIRequest.py @@ -1,20 +1,26 @@ import asyncio import json from typing import Any, List, Optional +from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse import httpx from loguru import logger from langflow.base.curl.parse import parse_context from langflow.custom import Component -from langflow.io import DropdownInput, IntInput, NestedDictInput, Output, TextInput +from langflow.io import DropdownInput, IntInput, NestedDictInput, Output, TextInput, DataInput from langflow.schema import Data from langflow.schema.dotdict import dotdict class APIRequestComponent(Component): display_name = "API Request" - description = "Make HTTP requests given one or more URLs." + description = ( + "This component allows you to make HTTP requests to one or more URLs. " + "You can provide headers and body as either dictionaries or Data objects. " + "Additionally, you can append query parameters to the URLs.\n\n" + "**Note:** Check advanced options for more settings." + ) icon = "Globe" inputs = [ @@ -27,7 +33,7 @@ class APIRequestComponent(Component): TextInput( name="curl", display_name="Curl", - info="Paste a curl command to populate the fields.", + info="Paste a curl command to populate the fields. This will fill in the dictionary fields for headers and body.", advanced=False, refresh_button=True, ), @@ -36,17 +42,34 @@ class APIRequestComponent(Component): display_name="Method", options=["GET", "POST", "PATCH", "PUT"], value="GET", - info="The HTTP method to use.", + info="The HTTP method to use (GET, POST, PATCH, PUT).", ), - NestedDictInput( + DataInput( name="headers", - display_name="Headers", - info="The headers to send with the request.", + display_name="Headers Data", + info="The headers to send with the request as a Data object.", ), NestedDictInput( + name="headers_dict", + display_name="Headers Dictionary", + info="The headers to send with the request as a dictionary. This is populated when using the CURL field.", + advanced=True, + ), + DataInput( name="body", - display_name="Body", - info="The body to send with the request (for POST, PATCH, PUT).", + display_name="Body Data", + info="The body to send with the request as a Data object (for POST, PATCH, PUT).", + ), + NestedDictInput( + name="body_dict", + display_name="Body Dictionary", + info="The body to send with the request as a dictionary (for POST, PATCH, PUT). This is populated when using the CURL field.", + advanced=True, + ), + DataInput( + name="query_params", + display_name="Query Parameters", + info="The query parameters to append to the URL.", ), IntInput( name="timeout", @@ -65,13 +88,16 @@ class APIRequestComponent(Component): parsed = parse_context(curl) build_config["urls"]["value"] = [parsed.url] build_config["method"]["value"] = parsed.method.upper() - build_config["headers"]["value"] = dict(parsed.headers) + build_config["headers_dict"]["value"] = dict(parsed.headers) - try: - json_data = json.loads(parsed.data) - build_config["body"]["value"] = json_data - except json.JSONDecodeError as e: - print(e) + if parsed.data: + try: + json_data = json.loads(parsed.data) + build_config["body_dict"]["value"] = json_data + except json.JSONDecodeError as e: + logger.error(f"Error decoding JSON data: {e}") + else: + build_config["body_dict"]["value"] = None except Exception as exc: logger.error(f"Error parsing curl: {exc}") raise ValueError(f"Error parsing curl: {exc}") @@ -80,6 +106,11 @@ class APIRequestComponent(Component): def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): if field_name == "curl" and field_value: build_config = self.parse_curl(field_value, build_config) + # Apply the parsed values to the component fields + self.urls = build_config["urls"]["value"] + self.method = build_config["method"]["value"] + self.headers_dict = build_config["headers"]["value"] + self.body_dict = build_config["body"]["value"] return build_config async def make_request( @@ -96,7 +127,7 @@ class APIRequestComponent(Component): raise ValueError(f"Unsupported method: {method}") data = body if body else None - payload = json.dumps(data) + payload = json.dumps(data) if data else None try: response = await client.request(method, url, headers=headers, content=payload, timeout=timeout) try: @@ -130,36 +161,38 @@ class APIRequestComponent(Component): }, ) + def add_query_params(self, url: str, params: dict) -> str: + url_parts = list(urlparse(url)) + query = dict(parse_qsl(url_parts[4])) + query.update(params) + url_parts[4] = urlencode(query) + return urlunparse(url_parts) + async def make_requests(self) -> List[Data]: method = self.method urls = [url.strip() for url in self.urls if url.strip()] curl = self.curl - headers = self.headers or {} - body = self.body or {} + headers = self.headers if self.headers else (self.headers_dict or {}) + body = self.body if self.body else (self.body_dict or {}) timeout = self.timeout + query_params = self.query_params.data if self.query_params else {} if curl: - self._build_config = self.parse_curl(curl) + self._build_config = self.parse_curl(curl, dotdict()) if isinstance(headers, Data): - headers_dict = headers.data - else: - headers_dict = headers + headers = headers.data - bodies = [] - if body: - if not isinstance(body, list): - bodies = [body] - else: - bodies = body - bodies = [b.data if isinstance(b, Data) else b for b in bodies] + if isinstance(body, Data): + body = body.data - if len(urls) != len(bodies): - bodies += [None] * (len(urls) - len(bodies)) + bodies = [body] * len(urls) + + urls = [self.add_query_params(url, query_params) for url in urls] async with httpx.AsyncClient() as client: results = await asyncio.gather( - *[self.make_request(client, method, u, headers_dict, rec, timeout) for u, rec in zip(urls, bodies)] + *[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)] ) self.status = results return results