feat: enhance APIRequestComponent with new output methods (#7148)

* ✨ (Research Translation Loop.spec.ts): Increase timeout value by a factor of 3 for better reliability in waiting for element to appear
✨ (chatInputOutputUser-shard-1.spec.ts): Increase timeout value by a factor of 3 for better reliability in waiting for element to appear

* 🐛 (typescript_test.yml): adjust the maximum shard count to 10 to prevent exceeding the limit and optimize test execution.

* 🐛 (chatInputOutputUser-shard-1.spec.ts): increase timeout for waiting for "built successfully" text to improve test reliability

* ⬆️ (typescript_test.yml): increase maximum shard count to 15 for better test distribution
♻️ (Portfolio Website Code Generator.spec.ts): refactor test assertions to improve readability and maintainability

* 🐛 (typescript_test.yml): adjust the maximum shard count to 10 to prevent exceeding the limit of parallel test executions

* 🔧 (typescript_test.yml): Increase maximum shard count to 15 for better test distribution efficiency
🐛 (chatInputOutputUser-shard-1.spec.ts): Update timeout values for page element waits to prevent premature failures due to timing issues

* templates adjustments

* travel planning fix

* Update Travel Planning Agents.json

* fix templates

* ♻️ (Youtube Analysis.spec.ts): remove unused imports and cleanup code for better readability and maintainability

* json fix

* fix: update simple agent template (#7081)

* Update Simple Agent.json

* Update Simple Agent.json

* feat: update search agent template agent component (#7082)

* update agent component with the latest changes

* Update Search agent.json

* Update Search agent.json

* feat: enhance APIRequestComponent with new output methods

- Refactored output methods to include `as_data` and `as_message` for better data handling.
- Updated existing output method from `make_requests` to `as_data` for consistency.
- Improved metadata handling by merging dictionary results and storing non-dict results as 'data'.
- Added detailed docstrings for new methods to clarify their functionality.

* added the updated test class

* [autofix.ci] apply automated fixes

* 📝 (ContentBlockDisplay.tsx): wrap headerIcon element in a span with data-testid attribute for better accessibility
📝 (DurationDisplay.tsx): add data-testid attribute to the duration display element for testing purposes
📝 (Simple Agent.spec.ts, Social Media Agent.spec.ts, generalBugs-shard-9.spec.ts): update test assertions to improve readability and accuracy
📝 (chatInputOutput.spec.ts): add a skip test annotation and a todo comment for further investigation

* ✨ (typescript_test.yml): Add support for a new development suite in the test workflow
✨ (frontend): Add support for a new development suite in multiple test files

* 🔧 (.github/workflows/typescript_test.yml): ensure that the SUITES variable is valid JSON format to prevent errors and improve reliability

* ✨ (inputFileComponent/index.tsx): Refactor InputFileComponent to wrap Button component in a div for better structure and readability
🔧 (Vector Store.spec.ts): Add initialGPTsetup function to set up GPT environment variables for tests
🔧 (Vector Store.spec.ts): Refactor test to use initialGPTsetup function and improve readability
🔧 (add-new-api-keys.ts): Refactor addNewApiKeys function to handle multiple openai_api_key inputs
🔧 (remove-old-api-keys.ts): Refactor removeOldApiKeys function to click on the correct remove-icon-badge element

* ✨ Add support for running tests in serial mode with a delay between each test
🔧 Configure tests to run with a 3-second delay between each test run
🔧 Add a 7-second delay before starting tests in userSettings.spec.ts
🔧 Add a 3-second delay before the second event delivery mode test in withEventDeliveryModes.ts

* 📝 (userSettings.spec.ts): remove unnecessary console log message to improve test readability and maintainability

* 📝 (deploy-dropdown.tsx): Add data-testid attribute to shareable-playground element
✨ (index.tsx): Add useGetTypes hook to fetch types data when component is fetched
🔧 (publish-flow.spec.ts): Increase timeout for page.waitForSelector and page.waitForTimeout
🔧 (Vector Store.spec.ts): Refactor code to wait for dropdown to appear and be visible
🔧 (files-page.spec.ts): Refactor tests to run serially with a delay between each test and add comments to improve readability

* ✨ Add useGetTypes hook to fetch types data and support caching with checkCache option
🔧 Refactor useGetTypes hook to accept options object with checkCache property
🔧 Refactor useGetTypes hook to conditionally return cached data if available
🔧 Refactor useGetTypes hook to fetch types data with force_refresh query parameter
🔧 Refactor useGetTypes hook to handle errors and set types data
🔧 Refactor useGetTypes hook to improve query function and options handling
🔧 Refactor useGetTypes hook to optimize query function and options handling
🔧 Refactor useGetTypes hook to improve caching logic and error handling
🔧 Refactor useGetTypes hook to enhance caching mechanism and error handling
🔧 Refactor useGetTypes hook to improve data fetching and error handling
🔧 Refactor useGetTypes hook to optimize data fetching and error handling

* ✨ (Vector Store.spec.ts): Remove unnecessary loadingOptions check and expectation
♻️ (withEventDeliveryModes.ts): Refactor withEventDeliveryModes function to accept a timeout parameter for better flexibility

* ⚡️ (Vector Store.spec.ts): increase timeout for page.waitForTimeout from 2000ms to 10000ms to improve test stability and reliability

* update pokedex agent template

* ✨ (publish-flow.spec.ts): Remove unnecessary development tag from test description
✨ (Simple Agent.spec.ts, Social Media Agent.spec.ts): Remove unnecessary development tag from test description
✨ (Vector Store.spec.ts): Change withEventDeliveryModes to test for better test organization
🔧 (chatInputOutput.spec.ts): Refactor test description and remove unnecessary development tag
✨ (files-page.spec.ts): Remove unnecessary development tag from test descriptions
✨ (userSettings.spec.ts): Remove unnecessary development tag from test description

* formatting json

* ✨ (Vector Store.spec.ts): Add new integration test withEventDeliveryModes for Vector Store RAG
🔧 (Vector Store.spec.ts): Update timeout values in test functions to improve test performance and reliability

* 📝 (Text Sentiment Analysis.json): Update JSON file to have consistent formatting and structure for output_types and inputTypes arrays
📝 (Text Sentiment Analysis.spec.ts): Refactor integration test for Text Sentiment Analysis to improve readability and maintainability

* ✨ (PageComponent/index.tsx): Update minZoom and maxZoom values for better user experience
🐛 (upload-file.ts): Fix missing await keyword before clicking on an element

* 🐛 (PageComponent/index.tsx): fix minZoom value to 0.2 for consistency with fitViewOptions and improve user experience

* ✨ (dropdownComponent.spec.ts): add delay before checking dropdown value to ensure it has updated properly
✨ (dropdownComponent.spec.ts): add delay before interacting with more options modal to ensure it has loaded
✨ (floatComponent.spec.ts): add delay after clicking add button to wait for API request to complete

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rodrigo Nader 2025-03-20 19:46:19 -03:00 • committed by GitHub
commit 44254206b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1183 additions and 727 deletions

View file

@ -26,9 +26,7 @@ from langflow.io import (
StrInput,
TableInput,
)
from langflow.schema import Data
from langflow.schema.dataframe import DataFrame
from langflow.schema.dotdict import dotdict
from langflow.schema import Data, DataFrame, Message, dotdict
class APIRequestComponent(Component):
@ -156,8 +154,9 @@ class APIRequestComponent(Component):
]
outputs = [
Output(display_name="Data", name="data", method="make_requests"),
Output(display_name="Data", name="data", method="as_data"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="Message", name="message", method="as_message"),
]
def _parse_json_value(self, value: Any) -> Any:
@ -454,7 +453,12 @@ class APIRequestComponent(Component):
self.log("Failed to decode JSON response")
result = response.text.encode("utf-8")
metadata.update({"result": result})
# If result is a dictionary, merge it with metadata
if isinstance(result, dict):
metadata.update(result)
else:
# If result is not a dict, store it as 'data'
metadata["data"] = result
if include_httpx_metadata:
metadata.update(
@ -528,7 +532,7 @@ class APIRequestComponent(Component):
urls = [self.add_query_params(url, query_params) for url in urls]
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
return await asyncio.gather(
*[
self.make_request(
client,
@ -544,8 +548,6 @@ class APIRequestComponent(Component):
for u, rec in zip(urls, bodies, strict=False)
]
)
self.status = results
return results
async def _response_info(
self, response: httpx.Response, *, with_file_path: bool = False
@ -644,6 +646,16 @@ class APIRequestComponent(Component):
return processed_headers
return {}
async def as_data(self) -> Data:
"""Convert the API response data into a DataFrame.
Returns:
DataFrame: A DataFrame containing the API response data.
"""
data = await self.make_requests()
dicts = {"output": [d.data for d in data]}
return Data(**dicts)
async def as_dataframe(self) -> DataFrame:
"""Convert the API response data into a DataFrame.
@ -652,3 +664,12 @@ class APIRequestComponent(Component):
"""
data = await self.make_requests()
return DataFrame(data)
async def as_message(self) -> Message:
"""Convert the API response data into a DataFrame.
Returns:
DataFrame: A DataFrame containing the API response data.
"""
data = await self.as_data()
return Message(text=str(data))

File diff suppressed because one or more lines are too long

View file

@ -9,16 +9,12 @@
"dataType": "File",
"id": "File-Ktatn",
"name": "data",
"output_types": [
"Data"
]
"output_types": ["Data"]
},
"targetHandle": {
"fieldName": "data",
"id": "ParseData-gouVC",
"inputTypes": [
"Data"
],
"inputTypes": ["Data"],
"type": "other"
}
},
@ -37,16 +33,12 @@
"dataType": "ParseData",
"id": "ParseData-gouVC",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "text",
"id": "Prompt-epiSD",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -65,16 +57,12 @@
"dataType": "Prompt",
"id": "Prompt-epiSD",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-ppS3O",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -93,16 +81,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-ppS3O",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "summary",
"id": "Prompt-l9XAo",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -121,16 +105,12 @@
"dataType": "Prompt",
"id": "Prompt-l9XAo",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-DxfrQ",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -149,16 +129,12 @@
"dataType": "Prompt",
"id": "Prompt-LKleN",
"name": "prompt",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "system_message",
"id": "OpenAIModel-W1vhv",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -177,16 +153,12 @@
"dataType": "ParseData",
"id": "ParseData-gouVC",
"name": "text",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "OpenAIModel-W1vhv",
"inputTypes": [
"Message"
],
"inputTypes": ["Message"],
"type": "str"
}
},
@ -205,18 +177,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-W1vhv",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-V5ZFA",
"inputTypes": [
"Data",
"DataFrame",
"Message"
],
"inputTypes": ["Data", "DataFrame", "Message"],
"type": "other"
}
},
@ -235,18 +201,12 @@
"dataType": "OpenAIModel",
"id": "OpenAIModel-DxfrQ",
"name": "text_output",
"output_types": [
"Message"
]
"output_types": ["Message"]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-8y94b",
"inputTypes": [
"Data",
"DataFrame",
"Message"
],
"inputTypes": ["Data", "DataFrame", "Message"],
"type": "other"
}
},
@ -263,9 +223,7 @@
"data": {
"id": "File-Ktatn",
"node": {
"base_classes": [
"Data"
],
"base_classes": ["Data"],
"beta": false,
"category": "data",
"conditional_paths": [],
@ -302,9 +260,31 @@
"required_inputs": [],
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
},
{
"allows_loop": false,
"cache": true,
"display_name": "DataFrame",
"method": "load_dataframe",
"name": "dataframe",
"required_inputs": [],
"selected": "DataFrame",
"tool_mode": true,
"types": ["DataFrame"],
"value": "__UNDEFINED__"
},
{
"allows_loop": false,
"cache": true,
"display_name": "Message",
"method": "load_message",
"name": "message",
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -372,10 +352,7 @@
"display_name": "Server File Path",
"dynamic": false,
"info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.",
"input_types": [
"Data",
"Message"
],
"input_types": ["Data", "Message"],
"list": true,
"list_add_label": "Add More",
"name": "file_path",
@ -426,7 +403,7 @@
"path": {
"_input_type": "FileInput",
"advanced": false,
"display_name": "Path",
"display_name": "Files",
"dynamic": false,
"fileTypes": [
"txt",
@ -455,7 +432,7 @@
],
"file_path": "43bb2a52-8dbf-4edf-a200-c54ee0e7fa1f\\2025-02-21_09-35-24_messages.json",
"info": "Supported file extensions: txt, md, mdx, csv, json, yaml, yml, xml, html, htm, pdf, docx, py, sh, sql, js, ts, tsx; optionally bundled in file extensions: zip, tar, tgz, bz2, gz",
"list": false,
"list": true,
"list_add_label": "Add More",
"name": "path",
"placeholder": "",
@ -466,6 +443,25 @@
"type": "file",
"value": ""
},
"separator": {
"_input_type": "StrInput",
"advanced": true,
"display_name": "Separator",
"dynamic": false,
"info": "Specify the separator to use between multiple outputs in Message format.",
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "separator",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"type": "str",
"value": "\n\n"
},
"silent_errors": {
"_input_type": "BoolInput",
"advanced": true,
@ -525,10 +521,7 @@
"data": {
"id": "ParseData-gouVC",
"node": {
"base_classes": [
"Data",
"Message"
],
"base_classes": ["Data", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -536,11 +529,7 @@
"display_name": "Data to Message",
"documentation": "",
"edited": false,
"field_order": [
"data",
"template",
"sep"
],
"field_order": ["data", "template", "sep"],
"frozen": false,
"icon": "message-square",
"legacy": false,
@ -559,9 +548,7 @@
"name": "text",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -572,9 +559,7 @@
"name": "data_list",
"selected": "Data",
"tool_mode": true,
"types": [
"Data"
],
"types": ["Data"],
"value": "__UNDEFINED__"
}
],
@ -605,9 +590,7 @@
"display_name": "Data",
"dynamic": false,
"info": "The data to convert to text.",
"input_types": [
"Data"
],
"input_types": ["Data"],
"list": true,
"list_add_label": "Add More",
"name": "data",
@ -646,9 +629,7 @@
"display_name": "Template",
"dynamic": false,
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -662,7 +643,7 @@
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": "list of messages details: {text}"
"value": "list of messages details: {file_path}"
}
},
"tool_mode": false
@ -687,25 +668,18 @@
"data": {
"id": "Prompt-l9XAo",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"summary"
]
"template": ["summary"]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt",
"documentation": "",
"edited": false,
"error": null,
"field_order": [
"template",
"tool_placeholder"
],
"field_order": ["template", "tool_placeholder"],
"frozen": false,
"full_path": null,
"icon": "prompts",
@ -727,9 +701,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -762,9 +734,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -800,9 +770,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -840,25 +808,18 @@
"data": {
"id": "Prompt-epiSD",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"text"
]
"template": ["text"]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt",
"documentation": "",
"edited": false,
"error": null,
"field_order": [
"template",
"tool_placeholder"
],
"field_order": ["template", "tool_placeholder"],
"frozen": false,
"full_path": null,
"icon": "prompts",
@ -880,9 +841,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -933,9 +892,7 @@
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"load_from_db": false,
"multiline": true,
@ -953,9 +910,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -993,10 +948,7 @@
"data": {
"id": "OpenAIModel-ppS3O",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1036,9 +988,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1047,14 +997,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1067,9 +1013,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": false,
"name": "api_key",
"password": true,
@ -1096,7 +1040,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[1],\n combobox=True,\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n required=True,\n ),\n SliderInput(\n name=\"temperature\", display_name=\"Temperature\", value=0.1, range_spec=RangeSpec(min=0, max=1, step=0.01)\n ),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n info=\"The maximum number of retries to make when generating.\",\n advanced=True,\n value=5,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"The timeout for requests to OpenAI completion API.\",\n advanced=True,\n value=700,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = self.json_mode\n seed = self.seed\n max_retries = self.max_retries\n timeout = self.timeout\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n max_retries=max_retries,\n request_timeout=timeout,\n )\n if json_mode:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n"
"value": "from langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[1],\n combobox=True,\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n required=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n info=\"The maximum number of retries to make when generating.\",\n advanced=True,\n value=5,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"The timeout for requests to OpenAI completion API.\",\n advanced=True,\n value=700,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = self.json_mode\n seed = self.seed\n max_retries = self.max_retries\n timeout = self.timeout\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n max_retries=max_retries,\n request_timeout=timeout,\n )\n if json_mode:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n"
},
"input_value": {
"_input_type": "MessageInput",
@ -1104,9 +1048,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1266,7 +1208,7 @@
},
"stream": {
"_input_type": "BoolInput",
"advanced": false,
"advanced": true,
"display_name": "Stream",
"dynamic": false,
"info": "Stream the response from the model. Streaming works only in Chat.",
@ -1288,9 +1230,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1308,7 +1248,7 @@
},
"temperature": {
"_input_type": "SliderInput",
"advanced": false,
"advanced": true,
"display_name": "Temperature",
"dynamic": false,
"info": "",
@ -1375,10 +1315,7 @@
"data": {
"id": "OpenAIModel-DxfrQ",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1418,9 +1355,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1429,14 +1364,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1449,9 +1380,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": false,
"name": "api_key",
"password": true,
@ -1478,7 +1407,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[1],\n combobox=True,\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n required=True,\n ),\n SliderInput(\n name=\"temperature\", display_name=\"Temperature\", value=0.1, range_spec=RangeSpec(min=0, max=1, step=0.01)\n ),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n info=\"The maximum number of retries to make when generating.\",\n advanced=True,\n value=5,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"The timeout for requests to OpenAI completion API.\",\n advanced=True,\n value=700,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = self.json_mode\n seed = self.seed\n max_retries = self.max_retries\n timeout = self.timeout\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n max_retries=max_retries,\n request_timeout=timeout,\n )\n if json_mode:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n"
"value": "from langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[1],\n combobox=True,\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n required=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n info=\"The maximum number of retries to make when generating.\",\n advanced=True,\n value=5,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"The timeout for requests to OpenAI completion API.\",\n advanced=True,\n value=700,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = self.json_mode\n seed = self.seed\n max_retries = self.max_retries\n timeout = self.timeout\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n max_retries=max_retries,\n request_timeout=timeout,\n )\n if json_mode:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n"
},
"input_value": {
"_input_type": "MessageInput",
@ -1486,9 +1415,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1648,7 +1575,7 @@
},
"stream": {
"_input_type": "BoolInput",
"advanced": false,
"advanced": true,
"display_name": "Stream",
"dynamic": false,
"info": "Stream the response from the model. Streaming works only in Chat.",
@ -1670,9 +1597,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1690,7 +1615,7 @@
},
"temperature": {
"_input_type": "SliderInput",
"advanced": false,
"advanced": true,
"display_name": "Temperature",
"dynamic": false,
"info": "",
@ -1757,9 +1682,7 @@
"data": {
"id": "Prompt-LKleN",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {
@ -1770,10 +1693,7 @@
"documentation": "",
"edited": false,
"error": null,
"field_order": [
"template",
"tool_placeholder"
],
"field_order": ["template", "tool_placeholder"],
"frozen": false,
"full_path": null,
"icon": "prompts",
@ -1795,9 +1715,7 @@
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -1846,9 +1764,7 @@
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -1886,10 +1802,7 @@
"data": {
"id": "OpenAIModel-W1vhv",
"node": {
"base_classes": [
"LanguageModel",
"Message"
],
"base_classes": ["LanguageModel", "Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -1929,9 +1842,7 @@
"required_inputs": [],
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
},
{
@ -1940,14 +1851,10 @@
"display_name": "Language Model",
"method": "build_model",
"name": "model_output",
"required_inputs": [
"api_key"
],
"required_inputs": ["api_key"],
"selected": "LanguageModel",
"tool_mode": true,
"types": [
"LanguageModel"
],
"types": ["LanguageModel"],
"value": "__UNDEFINED__"
}
],
@ -1960,9 +1867,7 @@
"display_name": "OpenAI API Key",
"dynamic": false,
"info": "The OpenAI API Key to use for the OpenAI model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"load_from_db": false,
"name": "api_key",
"password": true,
@ -1989,7 +1894,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[1],\n combobox=True,\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n required=True,\n ),\n SliderInput(\n name=\"temperature\", display_name=\"Temperature\", value=0.1, range_spec=RangeSpec(min=0, max=1, step=0.01)\n ),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n info=\"The maximum number of retries to make when generating.\",\n advanced=True,\n value=5,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"The timeout for requests to OpenAI completion API.\",\n advanced=True,\n value=700,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = self.json_mode\n seed = self.seed\n max_retries = self.max_retries\n timeout = self.timeout\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n max_retries=max_retries,\n request_timeout=timeout,\n )\n if json_mode:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n"
"value": "from langchain_openai import ChatOpenAI\nfrom pydantic.v1 import SecretStr\n\nfrom langflow.base.models.model import LCModelComponent\nfrom langflow.base.models.openai_constants import OPENAI_MODEL_NAMES\nfrom langflow.field_typing import LanguageModel\nfrom langflow.field_typing.range_spec import RangeSpec\nfrom langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput\n\n\nclass OpenAIModelComponent(LCModelComponent):\n display_name = \"OpenAI\"\n description = \"Generates text using OpenAI LLMs.\"\n icon = \"OpenAI\"\n name = \"OpenAIModel\"\n\n inputs = [\n *LCModelComponent._base_inputs,\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n advanced=True,\n info=\"The maximum number of tokens to generate. Set to 0 for unlimited tokens.\",\n range_spec=RangeSpec(min=0, max=128000),\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n BoolInput(\n name=\"json_mode\",\n display_name=\"JSON Mode\",\n advanced=True,\n info=\"If True, it will output JSON regardless of passing a schema.\",\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n advanced=False,\n options=OPENAI_MODEL_NAMES,\n value=OPENAI_MODEL_NAMES[1],\n combobox=True,\n ),\n StrInput(\n name=\"openai_api_base\",\n display_name=\"OpenAI API Base\",\n advanced=True,\n info=\"The base URL of the OpenAI API. \"\n \"Defaults to https://api.openai.com/v1. \"\n \"You can change this to use other APIs like JinaChat, LocalAI and Prem.\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"OpenAI API Key\",\n info=\"The OpenAI API Key to use for the OpenAI model.\",\n advanced=False,\n value=\"OPENAI_API_KEY\",\n required=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"seed\",\n display_name=\"Seed\",\n info=\"The seed controls the reproducibility of the job.\",\n advanced=True,\n value=1,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n info=\"The maximum number of retries to make when generating.\",\n advanced=True,\n value=5,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"The timeout for requests to OpenAI completion API.\",\n advanced=True,\n value=700,\n ),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n openai_api_key = self.api_key\n temperature = self.temperature\n model_name: str = self.model_name\n max_tokens = self.max_tokens\n model_kwargs = self.model_kwargs or {}\n openai_api_base = self.openai_api_base or \"https://api.openai.com/v1\"\n json_mode = self.json_mode\n seed = self.seed\n max_retries = self.max_retries\n timeout = self.timeout\n\n api_key = SecretStr(openai_api_key).get_secret_value() if openai_api_key else None\n output = ChatOpenAI(\n max_tokens=max_tokens or None,\n model_kwargs=model_kwargs,\n model=model_name,\n base_url=openai_api_base,\n api_key=api_key,\n temperature=temperature if temperature is not None else 0.1,\n seed=seed,\n max_retries=max_retries,\n request_timeout=timeout,\n )\n if json_mode:\n output = output.bind(response_format={\"type\": \"json_object\"})\n\n return output\n\n def _get_exception_message(self, e: Exception):\n \"\"\"Get a message from an OpenAI exception.\n\n Args:\n e (Exception): The exception to get the message from.\n\n Returns:\n str: The message from the exception.\n \"\"\"\n try:\n from openai import BadRequestError\n except ImportError:\n return None\n if isinstance(e, BadRequestError):\n message = e.body.get(\"message\")\n if message:\n return message\n return None\n"
},
"input_value": {
"_input_type": "MessageInput",
@ -1997,9 +1902,7 @@
"display_name": "Input",
"dynamic": false,
"info": "",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2159,7 +2062,7 @@
},
"stream": {
"_input_type": "BoolInput",
"advanced": false,
"advanced": true,
"display_name": "Stream",
"dynamic": false,
"info": "Stream the response from the model. Streaming works only in Chat.",
@ -2181,9 +2084,7 @@
"display_name": "System Message",
"dynamic": false,
"info": "System message to pass to the model.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2201,7 +2102,7 @@
},
"temperature": {
"_input_type": "SliderInput",
"advanced": false,
"advanced": true,
"display_name": "Temperature",
"dynamic": false,
"info": "",
@ -2295,9 +2196,7 @@
"data": {
"id": "ChatOutput-V5ZFA",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -2333,9 +2232,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -2348,9 +2245,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2371,9 +2266,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2430,9 +2323,7 @@
"display_name": "Data Template",
"dynamic": false,
"info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2453,11 +2344,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Data",
"DataFrame",
"Message"
],
"input_types": ["Data", "DataFrame", "Message"],
"list": false,
"list_add_label": "Add More",
"name": "input_value",
@ -2478,10 +2365,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -2498,9 +2382,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2521,9 +2403,7 @@
"display_name": "Session ID",
"dynamic": false,
"info": "The session ID of the chat. If empty, the current session ID parameter will be used.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2562,9 +2442,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2602,9 +2480,7 @@
"data": {
"id": "ChatOutput-8y94b",
"node": {
"base_classes": [
"Message"
],
"base_classes": ["Message"],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
@ -2640,9 +2516,7 @@
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"types": ["Message"],
"value": "__UNDEFINED__"
}
],
@ -2655,9 +2529,7 @@
"display_name": "Background Color",
"dynamic": false,
"info": "The background color of the icon.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2678,9 +2550,7 @@
"display_name": "Icon",
"dynamic": false,
"info": "The icon of the message.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2737,9 +2607,7 @@
"display_name": "Data Template",
"dynamic": false,
"info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2760,11 +2628,7 @@
"display_name": "Text",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Data",
"DataFrame",
"Message"
],
"input_types": ["Data", "DataFrame", "Message"],
"list": false,
"list_add_label": "Add More",
"name": "input_value",
@ -2785,10 +2649,7 @@
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options": ["Machine", "User"],
"options_metadata": [],
"placeholder": "",
"required": false,
@ -2805,9 +2666,7 @@
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2828,9 +2687,7 @@
"display_name": "Session ID",
"dynamic": false,
"info": "The session ID of the chat. If empty, the current session ID parameter will be used.",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2869,9 +2726,7 @@
"display_name": "Text Color",
"dynamic": false,
"info": "The text color of the name",
"input_types": [
"Message"
],
"input_types": ["Message"],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
@ -2999,7 +2854,5 @@
"is_component": false,
"last_tested_version": "1.2.0",
"name": "Text Sentiment Analysis",
"tags": [
"classification"
]
}
"tags": ["classification"]
}

View file

@ -1,6 +1,5 @@
import tempfile
from pathlib import Path
from unittest.mock import Mock
from unittest.mock import patch
import aiofiles
import aiofiles.os
@ -8,172 +7,306 @@ import httpx
import pytest
import respx
from httpx import Response
from langflow.components import data
from langflow.components.data import APIRequestComponent
from langflow.schema import Data, DataFrame, Message
from tests.base import ComponentTestBaseWithoutClient
@pytest.fixture
def api_request():
# This fixture provides an instance of APIRequest for each test case
return data.APIRequestComponent()
class TestAPIRequestComponent(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self):
"""Return the component class to test."""
return APIRequestComponent
@pytest.fixture
def default_kwargs(self):
"""Return the default kwargs for the component."""
return {
"urls": ["https://example.com/api/test"],
"method": "GET",
"headers": [],
"body": [],
"timeout": 5,
"follow_redirects": True,
"save_to_file": False,
"include_httpx_metadata": False,
"use_curl": False,
"curl": "",
}
def test_parse_curl(api_request):
# Arrange
field_value = (
"curl -X GET https://example.com/api/test -H 'Content-Type: application/json' -d '{\"key\": \"value\"}'"
)
build_config = {
"method": {"value": ""},
"urls": {"value": []},
"headers": {},
"body": {},
}
# Act
new_build_config = api_request.parse_curl(field_value, build_config.copy())
@pytest.fixture
def file_names_mapping(self):
"""Return an empty list since this component doesn't have version-specific files."""
return []
# Assert
assert new_build_config["method"]["value"] == "GET"
assert new_build_config["urls"]["value"] == ["https://example.com/api/test"]
assert new_build_config["headers"]["value"] == [{"key": "Content-Type", "value": "application/json"}]
assert new_build_config["body"]["value"] == [{"key": "key", "value": "value"}]
@pytest.fixture
async def component(self, component_class, default_kwargs):
"""Return a component instance."""
return component_class(**default_kwargs)
async def test_parse_curl(self, component):
# Test basic curl command parsing
curl_cmd = (
"curl -X GET https://example.com/api/test -H 'Content-Type: application/json' -d '{\"key\": \"value\"}'"
)
build_config = {
"method": {"value": ""},
"urls": {"value": []},
"headers": {},
"body": {},
}
new_build_config = component.parse_curl(curl_cmd, build_config.copy())
# HTTPx Metadata testing
@pytest.mark.parametrize(
("include_metadata", "expected_properties"),
[
(False, {"source", "result"}),
(True, {"source", "result", "headers", "status_code", "response_headers", "redirection_history"}),
],
)
@respx.mock
async def test_httpx_metadata_behavior(api_request, include_metadata, expected_properties):
# Mocking a successful GET request with headers and a redirection
url = "https://example.com/api/test"
redirected_url = "https://example.com/api/redirect"
response_content = {"key": "value"}
respx.get(url).mock(return_value=Response(303, headers={"Location": redirected_url}))
respx.get(redirected_url).mock(
return_value=Response(200, json=response_content, headers={"Custom-Header": "HeaderValue"})
)
assert new_build_config["method"]["value"] == "GET"
assert new_build_config["urls"]["value"] == ["https://example.com/api/test"]
assert new_build_config["headers"]["value"] == [{"key": "Content-Type", "value": "application/json"}]
assert new_build_config["body"]["value"] == [{"key": "key", "value": "value"}]
# Make the request
result = await api_request.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
save_to_file=False,
include_httpx_metadata=include_metadata,
)
@respx.mock
async def test_make_request_success(self, component):
# Test successful request with JSON response
url = "https://example.com/api/test"
response_data = {"key": "value"}
respx.get(url).mock(return_value=Response(200, json=response_data))
# Check returned metadata
metadata = result.data
assert set(metadata.keys()) == expected_properties, f"Unexpected properties: {set(metadata.keys())}"
if include_metadata:
# Validate individual fields
assert metadata["source"] == url
assert metadata["headers"] is None
assert metadata["status_code"] == 200
assert metadata["response_headers"]["custom-header"] == "HeaderValue"
# Validate redirection history
assert metadata["redirection_history"] == [{"url": redirected_url, "status_code": 303}], (
"Redirection history is incorrect"
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
)
# Validate result
assert metadata["result"] == response_content, "Response content mismatch"
assert isinstance(result, Data)
assert result.data["source"] == url
assert "key" in result.data
assert result.data["key"] == "value"
@respx.mock
async def test_make_request_with_metadata(self, component):
# Test request with metadata included
url = "https://example.com/api/test"
headers = {"Custom-Header": "Value"}
response_data = {"key": "value"}
respx.get(url).mock(return_value=Response(200, json=response_data, headers=headers))
# Save to File testing
@pytest.mark.parametrize(
("save_to_file", "expected_properties"),
[
(False, {"source", "result"}),
(True, {"source", "file_path"}),
],
)
@respx.mock
async def test_save_to_file_behavior(api_request, save_to_file, expected_properties):
# Mocking a successful GET request with a response body
url = "https://example.com/api/test"
response_content = "Test response content"
respx.get(url).mock(return_value=Response(200, content=response_content))
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
include_httpx_metadata=True,
)
# Make the request
result = await api_request.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
save_to_file=save_to_file,
)
assert isinstance(result, Data)
assert result.data["source"] == url
assert result.data["status_code"] == 200
assert result.data["response_headers"]["custom-header"] == "Value"
# Check returned metadata
metadata = result.data
assert set(metadata.keys()) == expected_properties, (
f"Unexpected properties: {set(metadata.keys())}. Raw result: {result.data}"
)
@respx.mock
async def test_make_request_save_to_file(self, component):
# Test saving response to file
url = "https://example.com/api/test"
content = "Test content"
respx.get(url).mock(return_value=Response(200, text=content))
if save_to_file:
# Validate that file_path exists in metadata
assert "file_path" in metadata, "file_path is missing in metadata"
file_path = metadata["file_path"]
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
save_to_file=True,
)
# Validate that the file exists and its content matches the response
assert await aiofiles.os.path.exists(file_path), "Saved file does not exist"
assert isinstance(result, Data)
assert "file_path" in result.data
file_path = Path(result.data["file_path"])
# Use async file operations
assert await aiofiles.os.path.exists(file_path)
async with aiofiles.open(file_path) as f:
file_content = await f.read()
assert file_content == response_content, "File content does not match response content"
saved_content = await f.read()
assert saved_content == content
# Cleanup the file
# Cleanup using async operation
await aiofiles.os.remove(file_path)
else:
# Validate that result exists in metadata
assert "result" in metadata, "result is missing in metadata"
assert metadata["result"] == response_content.encode("utf-8"), "Response content mismatch in metadata"
@respx.mock
async def test_make_request_binary_response(self, component):
# Test handling binary response
url = "https://example.com/api/binary"
binary_content = b"Binary content"
headers = {"Content-Type": "application/octet-stream"}
respx.get(url).mock(return_value=Response(200, content=binary_content, headers=headers))
async def test_response_info_binary_content(api_request):
response = Mock()
response.headers = {"Content-Type": "application/octet-stream"}
is_binary, file_path = await api_request._response_info(response, with_file_path=False)
assert is_binary is True
assert file_path is None
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
)
assert isinstance(result, Data)
assert result.data["source"] == url
assert result.data["data"] == binary_content
async def test_response_info_non_binary_content(api_request):
response = Mock()
response.headers = {"Content-Type": "text/plain"}
is_binary, file_path = await api_request._response_info(response, with_file_path=False)
assert is_binary is False
assert file_path is None
@respx.mock
async def test_make_request_timeout(self, component):
# Test request timeout
url = "https://example.com/api/test"
respx.get(url).mock(side_effect=httpx.TimeoutException("Request timed out"))
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
timeout=1,
)
async def test_response_info_filename_from_content_disposition(api_request):
response = Mock()
response.headers = {
"Content-Disposition": 'attachment; filename="thisfile.txt"',
"Content-Type": "text/plain",
}
response.request = Mock()
response.request.url = "https://example.com/testfile"
assert isinstance(result, Data)
assert result.data["status_code"] == 408
assert result.data["error"] == "Request timed out"
is_binary, file_path = await api_request._response_info(response, with_file_path=True)
@respx.mock
async def test_make_request_with_redirects(self, component):
# Test handling redirects
url = "https://example.com/api/test"
redirect_url = "https://example.com/api/redirect"
final_data = {"key": "value"}
assert is_binary is False
assert file_path.parent == Path(tempfile.gettempdir()) / "APIRequestComponent"
assert file_path.name.endswith("thisfile.txt")
respx.get(url).mock(return_value=Response(303, headers={"Location": redirect_url}))
respx.get(redirect_url).mock(return_value=Response(200, json=final_data))
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
include_httpx_metadata=True,
follow_redirects=True,
)
async def test_response_info_default_filename(api_request):
response = Mock()
response.headers = {"Content-Type": "text/plain"}
response.request = Mock()
response.request.url = "https://example.com/testfile"
assert isinstance(result, Data)
assert result.data["source"] == url
assert result.data["status_code"] == 200
assert result.data["redirection_history"] == [{"url": redirect_url, "status_code": 303}]
is_binary, file_path = await api_request._response_info(response, with_file_path=True)
async def test_process_headers(self, component):
# Test header processing
headers_list = [
{"key": "Content-Type", "value": "application/json"},
{"key": "Authorization", "value": "Bearer token"},
]
processed = component._process_headers(headers_list)
assert processed == {
"Content-Type": "application/json",
"Authorization": "Bearer token",
}
assert is_binary is False
assert file_path.parent == Path(tempfile.gettempdir()) / "APIRequestComponent"
assert file_path.name.endswith("testfile.txt")
# Test invalid headers
assert component._process_headers(None) == {}
assert component._process_headers([{"invalid": "format"}]) == {}
async def test_process_body(self, component):
# Test body processing
# Test dictionary body
dict_body = {"key": "value", "nested": {"inner": "value"}}
assert component._process_body(dict_body) == dict_body
# Test string body
json_str = '{"key": "value"}'
assert component._process_body(json_str) == {"key": "value"}
# Test list body
list_body = [{"key": "key1", "value": "value1"}, {"key": "key2", "value": "value2"}]
assert component._process_body(list_body) == {"key1": "value1", "key2": "value2"}
# Test invalid body
assert component._process_body(None) == {}
assert component._process_body([{"invalid": "format"}]) == {}
async def test_add_query_params(self, component):
# Test query parameter handling
url = "https://example.com/api/test"
params = {"param1": "value1", "param2": "value2"}
result = component.add_query_params(url, params)
assert "param1=value1" in result
assert "param2=value2" in result
# Test with existing query params
url_with_params = "https://example.com/api/test?existing=true"
result = component.add_query_params(url_with_params, params)
assert "existing=true" in result
assert "param1=value1" in result
assert "param2=value2" in result
async def test_output_formats(self, component):
# Test different output formats
with patch.object(component, "make_requests") as mock_make_requests:
mock_make_requests.return_value = [Data(data={"key": "value"})]
# Test DataFrame output
df_result = await component.as_dataframe()
assert isinstance(df_result, DataFrame)
# Test Message output
msg_result = await component.as_message()
assert isinstance(msg_result, Message)
# Test Data output
data_result = await component.as_data()
assert isinstance(data_result, Data)
assert isinstance(data_result.data["output"], list)
async def test_invalid_urls(self, component):
# Test invalid URL handling
component.urls = ["not_a_valid_url"]
with pytest.raises(ValueError, match="Invalid URLs provided"):
await component.make_requests()
async def test_update_build_config(self, component):
# Test build config updates
build_config = {
"method": {"value": "GET", "advanced": False},
"urls": {"value": [], "advanced": False},
"headers": {"value": [], "advanced": True},
"body": {"value": [], "advanced": True},
"use_curl": {"value": False, "advanced": False},
"curl": {"value": "", "advanced": True},
"timeout": {"value": 5, "advanced": True},
"follow_redirects": {"value": True, "advanced": True},
"save_to_file": {"value": False, "advanced": True},
"include_httpx_metadata": {"value": False, "advanced": True},
"query_params": {"value": {}, "advanced": True},
}
# Test curl mode update
updated = component.update_build_config(
build_config=build_config.copy(), field_value=True, field_name="use_curl"
)
assert updated["curl"]["advanced"] is False
assert updated["urls"]["advanced"] is True
# Test method update
updated = component.update_build_config(
build_config=build_config.copy(), field_value="POST", field_name="method"
)
assert updated["body"]["advanced"] is False
@respx.mock
async def test_error_handling(self, component):
# Test various error scenarios
url = "https://example.com/api/test"
# Test connection error
respx.get(url).mock(side_effect=httpx.ConnectError("Connection failed"))
result = await component.make_request(
client=httpx.AsyncClient(),
method="GET",
url=url,
)
assert result.data["status_code"] == 500
assert "Connection failed" in result.data["error"]
# Test invalid method
with pytest.raises(ValueError, match="Unsupported method"):
await component.make_request(
client=httpx.AsyncClient(),
method="INVALID",
url=url,
)