diff --git a/docs/docs/components/custom.mdx b/docs/docs/components/custom.mdx index ffa747c1b..90282a73e 100644 --- a/docs/docs/components/custom.mdx +++ b/docs/docs/components/custom.mdx @@ -33,6 +33,7 @@ The CustomComponent class serves as the foundation for creating custom component | Supported Types | | --------------------------------------------------------- | | _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ | + | _`langflow.field_typing.NestedDict`_ | | _`langchain.chains.base.Chain`_ | | _`langchain.PromptTemplate`_ | | _`langchain.llms.base.BaseLLM`_ | @@ -44,6 +45,8 @@ The CustomComponent class serves as the foundation for creating custom component | _`langchain.embeddings.base.Embeddings`_ | | _`langchain.schema.BaseRetriever`_ | + The difference between _`dict`_ and _`langflow.field_typing.NestedDict`_ is that one adds a simple key-value pair field, while the other opens a more robust dictionary editor. + Unlike Langchain types, base Python types do not add a [handle](../guidelines/components) to the field by default. To add handles, diff --git a/src/backend/langflow/components/chains/PromptRunner.py b/src/backend/langflow/components/chains/PromptRunner.py index 141941c38..db9283b07 100644 --- a/src/backend/langflow/components/chains/PromptRunner.py +++ b/src/backend/langflow/components/chains/PromptRunner.py @@ -16,17 +16,14 @@ class PromptRunner(CustomComponent): "info": "Make sure the prompt has all variables filled.", }, "code": {"show": False}, - "inputs": {"field_type": "code"}, } def build( - self, - llm: BaseLLM, - prompt: PromptTemplate, + self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {} ) -> Document: chain = prompt | llm # The input is an empty dict because the prompt is already filled - result = chain.invoke({}) + result = chain.invoke(input=inputs) if hasattr(result, "content"): result = result.content self.repr_value = result diff --git a/src/backend/langflow/components/retrievers/MetalRetriever.py b/src/backend/langflow/components/retrievers/MetalRetriever.py index 9b2d1e5dd..b105cd24f 100644 --- a/src/backend/langflow/components/retrievers/MetalRetriever.py +++ b/src/backend/langflow/components/retrievers/MetalRetriever.py @@ -14,7 +14,7 @@ class MetalRetrieverComponent(CustomComponent): "api_key": {"display_name": "API Key", "password": True}, "client_id": {"display_name": "Client ID", "password": True}, "index_id": {"display_name": "Index ID"}, - "params": {"display_name": "Parameters", "field_type": "code"}, + "params": {"display_name": "Parameters"}, "code": {"show": False}, } diff --git a/src/backend/langflow/components/utilities/GetRequest.py b/src/backend/langflow/components/utilities/GetRequest.py index 13ff0dc23..d5df32cca 100644 --- a/src/backend/langflow/components/utilities/GetRequest.py +++ b/src/backend/langflow/components/utilities/GetRequest.py @@ -19,7 +19,6 @@ class GetRequest(CustomComponent): }, "headers": { "display_name": "Headers", - "field_type": "code", "info": "The headers to send with the request.", }, "code": {"show": False}, diff --git a/src/backend/langflow/components/utilities/PostRequest.py b/src/backend/langflow/components/utilities/PostRequest.py index 4f7100d43..6857f4866 100644 --- a/src/backend/langflow/components/utilities/PostRequest.py +++ b/src/backend/langflow/components/utilities/PostRequest.py @@ -15,7 +15,6 @@ class PostRequest(CustomComponent): "url": {"display_name": "URL", "info": "The URL to make the request to."}, "headers": { "display_name": "Headers", - "field_type": "code", "info": "The headers to send with the request.", }, "code": {"show": False}, diff --git a/src/backend/langflow/components/utilities/UpdateRequest.py b/src/backend/langflow/components/utilities/UpdateRequest.py index 6e8991794..d18c94a56 100644 --- a/src/backend/langflow/components/utilities/UpdateRequest.py +++ b/src/backend/langflow/components/utilities/UpdateRequest.py @@ -15,7 +15,7 @@ class UpdateRequest(CustomComponent): "url": {"display_name": "URL", "info": "The URL to make the request to."}, "headers": { "display_name": "Headers", - "field_type": "code", + "field_type": "NestedDict", "info": "The headers to send with the request.", }, "code": {"show": False}, diff --git a/src/backend/langflow/field_typing/__init__.py b/src/backend/langflow/field_typing/__init__.py new file mode 100644 index 000000000..927716b11 --- /dev/null +++ b/src/backend/langflow/field_typing/__init__.py @@ -0,0 +1,3 @@ +from .base import NestedDict + +__all__ = ["NestedDict"] diff --git a/src/backend/langflow/field_typing/base.py b/src/backend/langflow/field_typing/base.py new file mode 100644 index 000000000..ed3219888 --- /dev/null +++ b/src/backend/langflow/field_typing/base.py @@ -0,0 +1,4 @@ +from typing import Union, Dict + +# Type alias for more complex dicts +NestedDict = Dict[str, Union[str, Dict]] diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index 0f9a5e8a9..ac05ac221 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -122,6 +122,19 @@ class Vertex: except Exception as exc: logger.debug(f"Error parsing code: {exc}") params[key] = value.get("value") + elif value.get("type") in ["dict", "NestedDict"]: + # When dict comes from the frontend it comes as a + # list of dicts, so we need to convert it to a dict + # before passing it to the build method + _value = value.get("value") + if isinstance(_value, list): + params[key] = { + k: v + for item in value.get("value", []) + for k, v in item.items() + } + elif isinstance(_value, dict): + params[key] = _value else: params[key] = value.get("value") diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index 1aff2f0e2..eb012e182 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -59,7 +59,7 @@ def get_memory_key(langchain_object): "history": "chat_history", } # Check if memory_key attribute exists - if hasattr(langchain_object.memory, 'memory_key'): + if hasattr(langchain_object.memory, "memory_key"): memory_key = langchain_object.memory.memory_key return mem_key_dict.get(memory_key) else: @@ -90,5 +90,4 @@ def update_memory_keys(langchain_object, possible_new_mem_key): try: setattr(langchain_object.memory, attr, key) except ValueError as exc: - logger.debug( - f"{langchain_object.memory} has no attribute {attr} ({exc})") + logger.debug(f"{langchain_object.memory} has no attribute {attr} ({exc})") diff --git a/src/backend/langflow/template/frontend_node/base.py b/src/backend/langflow/template/frontend_node/base.py index fe19b5652..442e2ffd7 100644 --- a/src/backend/langflow/template/frontend_node/base.py +++ b/src/backend/langflow/template/frontend_node/base.py @@ -140,13 +140,16 @@ class FrontendNode(BaseModel): @staticmethod def handle_dict_type(field: TemplateField, _type: str) -> str: """Handles 'dict' type by replacing it with 'code' or 'file' based on the field name.""" - if "dict" in _type.lower(): - if field.name == "dict_": - field.field_type = "file" - field.suffixes = [".json", ".yaml", ".yml"] - field.file_types = ["json", "yaml", "yml"] - else: - field.field_type = "code" + if "dict" in _type.lower() and field.name == "dict_": + field.field_type = "file" + field.suffixes = [".json", ".yaml", ".yml"] + field.file_types = ["json", "yaml", "yml"] + elif ( + _type.startswith("Dict") + or _type.startswith("Mapping") + or _type.startswith("dict") + ): + field.field_type = "dict" return _type @staticmethod @@ -240,20 +243,6 @@ class FrontendNode(BaseModel): "description", } - @staticmethod - def replace_dict_with_code_or_file( - field: TemplateField, _type: str, key: str - ) -> str: - """Replaces 'dict' type with 'code' or 'file'.""" - if "dict" in _type.lower(): - if key == "dict_": - field.field_type = "file" - field.suffixes = [".json", ".yaml", ".yml"] - field.file_types = ["json", "yaml", "yml"] - else: - field.field_type = "code" - return field.field_type - @staticmethod def set_field_default_value(field: TemplateField, value: dict, key: str) -> None: """Sets the field value with the default value if present.""" diff --git a/src/backend/langflow/template/frontend_node/documentloaders.py b/src/backend/langflow/template/frontend_node/documentloaders.py index cdf67e54a..8118593f3 100644 --- a/src/backend/langflow/template/frontend_node/documentloaders.py +++ b/src/backend/langflow/template/frontend_node/documentloaders.py @@ -170,11 +170,11 @@ class DocumentLoaderFrontNode(FrontendNode): # add a metadata field of type dict self.template.add_field( TemplateField( - field_type="code", + field_type="dict", required=True, show=True, name="metadata", - value="{}", + value={}, display_name="Metadata", multiline=False, ) diff --git a/src/backend/langflow/template/frontend_node/embeddings.py b/src/backend/langflow/template/frontend_node/embeddings.py index 4e7e25112..665328e78 100644 --- a/src/backend/langflow/template/frontend_node/embeddings.py +++ b/src/backend/langflow/template/frontend_node/embeddings.py @@ -89,7 +89,7 @@ class EmbeddingFrontendNode(FrontendNode): if field.name == "headers": field.show = False if field.name == "model_kwargs": - field.field_type = "code" + field.field_type = "dict" field.advanced = True field.show = True elif field.name in [ diff --git a/src/backend/langflow/template/frontend_node/formatter/field_formatters.py b/src/backend/langflow/template/frontend_node/formatter/field_formatters.py index 7987b134a..247f3b421 100644 --- a/src/backend/langflow/template/frontend_node/formatter/field_formatters.py +++ b/src/backend/langflow/template/frontend_node/formatter/field_formatters.py @@ -153,10 +153,13 @@ class DictCodeFileFormatter(FieldFormatter): key = field.name value = field.to_dict() _type = value["type"] - if "dict" in _type.lower(): - if key == "dict_": - field.field_type = "file" - field.suffixes = [".json", ".yaml", ".yml"] - field.file_types = ["json", "yaml", "yml"] - else: - field.field_type = "code" + if "dict" in _type.lower() and key == "dict_": + field.field_type = "file" + field.suffixes = [".json", ".yaml", ".yml"] + field.file_types = ["json", "yaml", "yml"] + elif ( + _type.startswith("Dict") + or _type.startswith("Mapping") + or _type.startswith("dict") + ): + field.field_type = "dict" diff --git a/src/backend/langflow/template/frontend_node/llms.py b/src/backend/langflow/template/frontend_node/llms.py index 01098724e..b8e007a27 100644 --- a/src/backend/langflow/template/frontend_node/llms.py +++ b/src/backend/langflow/template/frontend_node/llms.py @@ -131,7 +131,7 @@ class LLMFrontendNode(FrontendNode): if display_name := display_names_dict.get(field.name): field.display_name = display_name if field.name == "model_kwargs": - field.field_type = "code" + field.field_type = "dict" field.advanced = True field.show = True elif field.name in [ diff --git a/src/backend/langflow/template/frontend_node/prompts.py b/src/backend/langflow/template/frontend_node/prompts.py index da5d2a300..c52b1901c 100644 --- a/src/backend/langflow/template/frontend_node/prompts.py +++ b/src/backend/langflow/template/frontend_node/prompts.py @@ -15,6 +15,7 @@ from langflow.template.template.base import Template class PromptFrontendNode(FrontendNode): @staticmethod def format_field(field: TemplateField, name: Optional[str] = None) -> None: + FrontendNode.format_field(field, name) # if field.field_type == "StringPromptTemplate" # change it to str PROMPT_FIELDS = [ diff --git a/src/backend/langflow/template/frontend_node/utilities.py b/src/backend/langflow/template/frontend_node/utilities.py index 9dedacd0f..a5adb219d 100644 --- a/src/backend/langflow/template/frontend_node/utilities.py +++ b/src/backend/langflow/template/frontend_node/utilities.py @@ -21,5 +21,4 @@ class UtilitiesFrontendNode(FrontendNode): field.field_type = "str" if isinstance(field.value, dict): - field.field_type = "code" field.value = orjson_dumps(field.value) diff --git a/src/backend/langflow/template/frontend_node/vectorstores.py b/src/backend/langflow/template/frontend_node/vectorstores.py index 23c293437..73e9aaaca 100644 --- a/src/backend/langflow/template/frontend_node/vectorstores.py +++ b/src/backend/langflow/template/frontend_node/vectorstores.py @@ -56,7 +56,7 @@ class VectorStoreFrontendNode(FrontendNode): # Add search_kwargs field extra_field = TemplateField( name="search_kwargs", - field_type="code", + field_type="NestedDict", required=False, placeholder="", show=True, diff --git a/src/backend/langflow/utils/constants.py b/src/backend/langflow/utils/constants.py index e473d855b..43f92f651 100644 --- a/src/backend/langflow/utils/constants.py +++ b/src/backend/langflow/utils/constants.py @@ -48,4 +48,14 @@ def python_function(text: str) -> str: return text """ -DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"] +DIRECT_TYPES = [ + "str", + "bool", + "dict", + "int", + "float", + "Any", + "prompt", + "code", + "NestedDict", +] diff --git a/src/backend/langflow/utils/util.py b/src/backend/langflow/utils/util.py index 921f913c6..4fd9350d2 100644 --- a/src/backend/langflow/utils/util.py +++ b/src/backend/langflow/utils/util.py @@ -276,8 +276,6 @@ def format_dict( value["password"] = is_password_field(key) value["multiline"] = is_multiline_field(key) - replace_dict_type_with_code(value) - if key == "dict_": set_dict_file_attributes(value) @@ -407,14 +405,6 @@ def is_multiline_field(key: str) -> bool: } -def replace_dict_type_with_code(value: Dict[str, Any]) -> None: - """ - Replaces the type value with 'code' if the type is a dict. - """ - if "dict" in value["type"].lower(): - value["type"] = "code" - - def set_dict_file_attributes(value: Dict[str, Any]) -> None: """ Sets the file attributes for the 'dict_' key. diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 1c029be70..ccdd0dc73 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -58,6 +58,7 @@ "react-syntax-highlighter": "^15.5.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.21.1", + "react18-json-view": "^0.2.3", "reactflow": "^11.8.3", "rehype-mathjax": "^4.0.3", "remark-gfm": "^3.0.1", @@ -8872,6 +8873,14 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react18-json-view": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/react18-json-view/-/react18-json-view-0.2.3.tgz", + "integrity": "sha512-FZlEWS6A3yf1CyuPRtxYrGNZVRvUT1jQvt/kS1C3SDrStwViENsAKhe3NANq37on9cPoNbLD8+KZB+EArNhuWQ==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/reactflow": { "version": "11.8.3", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.8.3.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 8d7d25d88..8dcc200de 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -53,6 +53,7 @@ "react-syntax-highlighter": "^15.5.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.21.1", + "react18-json-view": "^0.2.3", "reactflow": "^11.8.3", "rehype-mathjax": "^4.0.3", "remark-gfm": "^3.0.1", diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index be7173d7f..fc28a9c83 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -3,45 +3,78 @@ @tailwind utilities; .App { - text-align: center; + text-align: center; } .App-logo { - height: 40vmin; - pointer-events: none; + height: 40vmin; + pointer-events: none; } @media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } + .App-logo { + animation: App-logo-spin infinite 20s linear; + } } .App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; } .App-link { - color: #61dafb; + color: #61dafb; } @keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } @font-face { - font-family: text-security-disc; - src: url("assets/text-security-disc.woff") format("woff"); + font-family: text-security-disc; + src: url("assets/text-security-disc.woff") format("woff"); +} + +.json-view { + height: 370px !important; + background-color: #2c2c2c !important; + border-radius: 10px !important; + padding: 10px !important; +} + +.jv-indent { + overflow-y: auto !important; + max-height: 310px !important; + border-radius: 10px; +} + +.jv-indent::-webkit-scrollbar { + width: 8px !important; + height: 8px !important; + border-radius: 10px; +} + +.jv-indent::-webkit-scrollbar-track { + background-color: #f1f1f1 !important; + border-radius: 10px; +} + +.jv-indent::-webkit-scrollbar-thumb { + background-color: #ccc !important; + border-radius: 999px !important; +} + +.jv-indent::-webkit-scrollbar-thumb:hover { + background-color: #bbb !important; } diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index d71a18ebb..08eb261a6 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -9,6 +9,7 @@ import React, { import { Handle, Position, useUpdateNodeInternals } from "reactflow"; import ShadTooltip from "../../../../components/ShadTooltipComponent"; import CodeAreaComponent from "../../../../components/codeAreaComponent"; +import DictComponent from "../../../../components/dictComponent"; import Dropdown from "../../../../components/dropdownComponent"; import FloatComponent from "../../../../components/floatComponent"; import IconComponent from "../../../../components/genericIconComponent"; @@ -16,6 +17,7 @@ import InputComponent from "../../../../components/inputComponent"; import InputFileComponent from "../../../../components/inputFileComponent"; import InputListComponent from "../../../../components/inputListComponent"; import IntComponent from "../../../../components/intComponent"; +import KeypairListComponent from "../../../../components/keypairListComponent"; import PromptAreaComponent from "../../../../components/promptComponent"; import TextAreaComponent from "../../../../components/textAreaComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent"; @@ -24,7 +26,12 @@ import { TabsContext } from "../../../../contexts/tabsContext"; import { typesContext } from "../../../../contexts/typesContext"; import { ParameterComponentType } from "../../../../types/components"; import { TabsState } from "../../../../types/tabs"; -import { isValidConnection } from "../../../../utils/reactflowUtils"; +import { + convertObjToArray, + convertValuesToNumbers, + hasDuplicateKeys, + isValidConnection, +} from "../../../../utils/reactflowUtils"; import { nodeColors, nodeIconsLucide, @@ -74,13 +81,18 @@ export default function ParameterComponent({ const { data: myData } = useContext(typesContext); - const handleOnNewValue = (newValue: string | string[] | boolean): void => { + const handleOnNewValue = ( + newValue: string | string[] | boolean | Object[] + ): void => { let newData = cloneDeep(data); newData.node!.template[name].value = newValue; setData(newData); // Set state to pending //@ts-ignore setTabsState((prev: TabsState) => { + if (!prev[tabId]) { + return prev; + } return { ...prev, [tabId]: { @@ -93,6 +105,8 @@ export default function ParameterComponent({ renderTooltips(); }; + const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); + useEffect(() => { if (name === "openai_api_base") console.log(info); // @ts-ignore @@ -214,6 +228,8 @@ export default function ParameterComponent({ type === "code" || type === "prompt" || type === "file" || + type === "dict" || + type === "NestedDict" || type === "int") && !optionalHandle ? ( <> @@ -350,6 +366,42 @@ export default function ParameterComponent({ onChange={handleOnNewValue} /> + ) : left === true && type === "NestedDict" ? ( +
+ { + data.node!.template[name].value = newValue; + handleOnNewValue(newValue); + }} + /> +
+ ) : left === true && type === "dict" ? ( +
+ { + const valueToNumbers = convertValuesToNumbers(newValue); + data.node!.template[name].value = valueToNumbers; + setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers)); + handleOnNewValue(valueToNumbers); + }} + /> +
) : ( <> )} diff --git a/src/frontend/src/components/dictComponent/index.tsx b/src/frontend/src/components/dictComponent/index.tsx new file mode 100644 index 000000000..981372506 --- /dev/null +++ b/src/frontend/src/components/dictComponent/index.tsx @@ -0,0 +1,53 @@ +import { useEffect } from "react"; +import { DictComponentType } from "../../types/components"; + +import DictAreaModal from "../../modals/dictAreaModal"; +import { classNames } from "../../utils/utils"; +import { Input } from "../ui/input"; + +export default function DictComponent({ + value, + onChange, + disabled, + editNode = false, +}: DictComponentType): JSX.Element { + useEffect(() => { + if (disabled) { + onChange([""]); + } + }, [disabled]); + + useEffect(() => { + if (value) onChange(value); + }, [value]); + + return ( +
1 && editNode ? "my-1" : "", + "flex flex-col gap-3" + )} + > + { +
+ { + onChange(obj); + }} + > + + +
+ } +
+ ); +} diff --git a/src/frontend/src/components/keypairListComponent/index.tsx b/src/frontend/src/components/keypairListComponent/index.tsx new file mode 100644 index 000000000..e9c016ece --- /dev/null +++ b/src/frontend/src/components/keypairListComponent/index.tsx @@ -0,0 +1,112 @@ +import { useEffect } from "react"; +import { KeyPairListComponentType } from "../../types/components"; + +import _ from "lodash"; +import { classNames } from "../../utils/utils"; +import IconComponent from "../genericIconComponent"; +import { Input } from "../ui/input"; + +export default function KeypairListComponent({ + value, + onChange, + disabled, + editNode = false, + duplicateKey, +}: KeyPairListComponentType): JSX.Element { + useEffect(() => { + if (disabled) { + onChange([""]); + } + }, [disabled]); + + const handleChangeKey = (event, idx) => { + const newInputList = _.cloneDeep(value); + const oldKey = Object.keys(newInputList[idx])[0]; + const updatedObj = { [event.target.value]: newInputList[idx][oldKey] }; + newInputList[idx] = updatedObj; + onChange(newInputList); + }; + + const handleChangeValue = (newValue, idx) => { + const newInputList = _.cloneDeep(value); + const key = Object.keys(newInputList[idx])[0]; + newInputList[idx][key] = newValue; + onChange(newInputList); + }; + + useEffect(() => { + if (value) onChange(value); + }, [value]); + + return ( +
1 && editNode ? "my-1" : "", + "flex flex-col gap-3" + )} + > + {value?.map((obj, index) => { + return Object.keys(obj).map((key, idx) => { + return ( +
+ handleChangeKey(event, index)} + onKeyDown={(e) => { + if (e.ctrlKey && e.key === "Backspace") { + e.preventDefault(); + e.stopPropagation(); + } + }} + /> + + + handleChangeValue(event.target.value, index) + } + /> + + {index === value.length - 1 ? ( + + ) : ( + + )} +
+ ); + }); + })} +
+ ); +} diff --git a/src/frontend/src/components/ui/dialog.tsx b/src/frontend/src/components/ui/dialog.tsx index a6d3d58e9..551fa281c 100644 --- a/src/frontend/src/components/ui/dialog.tsx +++ b/src/frontend/src/components/ui/dialog.tsx @@ -13,7 +13,7 @@ const DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => ( -
+
{children}
@@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef< {children} @@ -166,6 +191,52 @@ const EditNodeModal = forwardRef( handleOnNewValue(value, templateParam); }} /> + ) : myData.node?.template[templateParam] + .type === "NestedDict" ? ( +
+ { + setObj(newValue); + }} + /> +
+ ) : myData.node?.template[templateParam] + .type === "dict" ? ( +
+ { + setErrorDuplicateKey( + hasDuplicateKeys(newValue) + ); + if (hasDuplicateKeys(newValue)) { + setDictArr(newValue); + } else { + setDictArr(newValue); + myData.node!.template[ + templateParam + ].value = newValue; + } + }} + /> +
) : myData.node.template[templateParam] .multiline ? ( { + if (value) setDictObj(value); + }, [value]); + + return ( + + {children} + + Edit Dictionary + + +
+ { + setDictObj(edit["src"]); + }} + src={dictObj} + /> +
+ +
+
+
+
+ ); +} diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 0c80c9026..1a2882a71 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -1057,4 +1057,8 @@ .label-invalid{ @apply text-destructive } + + .input-invalid{ + @apply border-destructive focus:ring-destructive focus:border-destructive + } } diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 9b1db2ad2..1d0825f7f 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -54,6 +54,21 @@ export type InputListComponentType = { editNode?: boolean; }; +export type KeyPairListComponentType = { + value: any; + onChange: (value: Object[]) => void; + disabled: boolean; + editNode?: boolean; + duplicateKey?: boolean; +}; + +export type DictComponentType = { + value: any; + onChange: (value) => void; + disabled: boolean; + editNode?: boolean; +}; + export type TextAreaComponentType = { field_name?: string; nodeClass?: APIClassType; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 9d06de64c..df8c784c4 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -204,30 +204,49 @@ export function validateNode( node: { template }, } = node.data; - return Object.keys(template).reduce( - (errors: Array, t) => - errors.concat( - template[t].required && - template[t].show && - (template[t].value === undefined || - template[t].value === null || - template[t].value === "") && - !reactFlowInstance - .getEdges() - .some( - (edge) => - edge.targetHandle?.split("|")[1] === t && - edge.targetHandle.split("|")[2] === node.id - ) - ? [ - `${type} is missing ${ - template.display_name || toNormalCase(template[t].name) - }.`, - ] - : [] - ), - [] as string[] - ); + return Object.keys(template).reduce((errors: Array, t) => { + if ( + template[t].required && + template[t].show && + (template[t].value === undefined || + template[t].value === null || + template[t].value === "") && + !reactFlowInstance + .getEdges() + .some( + (edge) => + edge.targetHandle?.split("|")[1] === t && + edge.targetHandle.split("|")[2] === node.id + ) + ) { + errors.push( + `${type} is missing ${ + template.display_name || toNormalCase(template[t].name) + }.` + ); + } else if ( + template[t].type === "dict" && + template[t].required && + template[t].show && + (template[t].value !== undefined || + template[t].value !== null || + template[t].value !== "") + ) { + if (hasDuplicateKeys(template[t].value)) + errors.push( + `${type} (${ + template.display_name || template[t].name + }) contains duplicate keys with the same values.` + ); + if (hasEmptyKey(template[t].value)) + errors.push( + `${type} (${ + template.display_name || template[t].name + }) field must not be empty.` + ); + } + return errors; + }, [] as string[]); } export function validateNodes(reactFlowInstance: ReactFlowInstance) { @@ -291,3 +310,58 @@ export function getConnectedNodes( const targetId = edge.target; return nodes.filter((node) => node.id === targetId || node.id === sourceId); } + +export function convertObjToArray(singleObject) { + if (Array.isArray(singleObject)) return singleObject; + + let arrConverted: any = []; + for (const key in singleObject) { + if (singleObject.hasOwnProperty(key)) { + const newObj = {}; + newObj[key] = singleObject[key]; + arrConverted.push(newObj); + } + } + return arrConverted; +} + +export function hasDuplicateKeys(array) { + const keys = {}; + for (const obj of array) { + for (const key in obj) { + if (keys[key]) { + return true; + } + keys[key] = true; + } + } + return false; +} + +export function hasEmptyKey(objArray) { + for (const obj of objArray) { + for (const key in obj) { + if (obj.hasOwnProperty(key) && key === "") { + return true; // Found an empty key + } + } + } + return false; // No empty keys found +} + +export function convertValuesToNumbers(arr) { + return arr.map((obj) => { + const newObj = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + let value = obj[key]; + if (/\s/g.test(value)) { + value = value.trim(); + } + newObj[key] = + value === "" || isNaN(value) ? value.toString() : Number(value); + } + } + return newObj; + }); +} diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 01b00b669..158ab9b36 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -1,6 +1,7 @@ import { ArrowUpToLine, Bell, + BookMarked, Check, CheckCircle2, ChevronDown, @@ -307,5 +308,6 @@ export const nodeIconsLucide: iconsType = { UserCog2, Key, Unplug, + BookMarked, ChevronUp, }; diff --git a/tests/test_llms_template.py b/tests/test_llms_template.py index 5bae17a5f..0a30a825e 100644 --- a/tests/test_llms_template.py +++ b/tests/test_llms_template.py @@ -1,97 +1,4 @@ from fastapi.testclient import TestClient -from langflow.services.utils import get_settings_manager - - -# def test_hugging_face_hub(client: TestClient): -# response = client.get("api/v1/all") -# assert response.status_code == 200 -# json_response = response.json() -# language_models = json_response["llms"] - -# model = language_models["HuggingFaceHub"] -# template = model["template"] - -# assert template["cache"] == { -# "required": False, -# "placeholder": "", -# "show": False, -# "multiline": False, -# "password": False, -# "name": "cache", -# "type": "bool", -# "list": False, -# "advanced": False, -# } -# assert template["verbose"] == { -# "required": False, -# "placeholder": "", -# "show": False, -# "multiline": False, -# "value": False, -# "password": False, -# "name": "verbose", -# "type": "bool", -# "list": False, -# "advanced": False, -# } -# assert template["client"] == { -# "required": False, -# "placeholder": "", -# "show": False, -# "multiline": False, -# "password": False, -# "name": "client", -# "type": "Any", -# "list": False, -# "advanced": False, -# } -# assert template["repo_id"] == { -# "required": False, -# "placeholder": "", -# "show": True, -# "multiline": False, -# "value": "gpt2", -# "password": False, -# "name": "repo_id", -# "type": "str", -# "list": False, -# "advanced": False, -# } -# assert template["task"] == { -# "required": True, -# "placeholder": "", -# "show": True, -# "multiline": False, -# "password": False, -# "options": ["text-generation", "text2text-generation"], -# "name": "task", -# "type": "str", -# "list": True, -# "advanced": True, -# } -# assert template["model_kwargs"] == { -# "required": False, -# "placeholder": "", -# "show": True, -# "multiline": False, -# "password": False, -# "name": "model_kwargs", -# "type": "code", -# "list": False, -# "advanced": True, -# } -# assert template["huggingfacehub_api_token"] == { -# "required": False, -# "placeholder": "", -# "show": True, -# "multiline": False, -# "password": True, -# "name": "huggingfacehub_api_token", -# "display_name": "HuggingFace Hub API Token", -# "type": "str", -# "list": False, -# "advanced": False, -# } def test_openai(client: TestClient, logged_in_headers): @@ -270,7 +177,7 @@ def test_openai(client: TestClient, logged_in_headers): "multiline": False, "password": False, "name": "model_kwargs", - "type": "code", + "type": "dict", "list": False, "advanced": True, "info": "", @@ -325,7 +232,7 @@ def test_openai(client: TestClient, logged_in_headers): "multiline": False, "password": False, "name": "logit_bias", - "type": "code", + "type": "dict", "list": False, "advanced": False, "info": "", @@ -442,7 +349,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers): "multiline": False, "password": False, "name": "model_kwargs", - "type": "code", + "type": "dict", "list": False, "advanced": True, "info": "", diff --git a/tests/test_prompts_template.py b/tests/test_prompts_template.py index 676448f73..434691038 100644 --- a/tests/test_prompts_template.py +++ b/tests/test_prompts_template.py @@ -55,7 +55,7 @@ def test_prompt_template(client: TestClient, logged_in_headers): "multiline": False, "password": False, "name": "partial_variables", - "type": "code", + "type": "dict", "list": False, "advanced": False, "info": "", diff --git a/tests/test_template.py b/tests/test_template.py index 4be6dff06..81f2a6020 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -135,7 +135,7 @@ def test_format_dict(): } expected_output = { "field1": { - "type": "code", # Mapping type is replaced with dict which is replaced with code + "type": "dict[str, int]", # Mapping type is replaced with dict which is replaced with code "required": False, "list": False, "show": False, @@ -249,7 +249,7 @@ def test_format_dict(): } expected_output = { "field1": { - "type": "code", + "type": "Dict[str, int]", "required": False, "list": False, "show": False,