Adding Support for Key-Value Pair Objects in Input Flows (#845)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-09-19 15:31:24 -03:00 • committed by GitHub
commit 311b0a57e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 619 additions and 207 deletions

View file

@ -33,6 +33,7 @@ The CustomComponent class serves as the foundation for creating custom component
| Supported Types | | Supported Types |
| --------------------------------------------------------- | | --------------------------------------------------------- |
| _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ | | _`str`_, _`int`_, _`float`_, _`bool`_, _`list`_, _`dict`_ |
| _`langflow.field_typing.NestedDict`_ |
| _`langchain.chains.base.Chain`_ | | _`langchain.chains.base.Chain`_ |
| _`langchain.PromptTemplate`_ | | _`langchain.PromptTemplate`_ |
| _`langchain.llms.base.BaseLLM`_ | | _`langchain.llms.base.BaseLLM`_ |
@ -44,6 +45,8 @@ The CustomComponent class serves as the foundation for creating custom component
| _`langchain.embeddings.base.Embeddings`_ | | _`langchain.embeddings.base.Embeddings`_ |
| _`langchain.schema.BaseRetriever`_ | | _`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.
<Admonition type="info"> <Admonition type="info">
Unlike Langchain types, base Python types do not add a Unlike Langchain types, base Python types do not add a
[handle](../guidelines/components) to the field by default. To add handles, [handle](../guidelines/components) to the field by default. To add handles,

View file

@ -16,17 +16,14 @@ class PromptRunner(CustomComponent):
"info": "Make sure the prompt has all variables filled.", "info": "Make sure the prompt has all variables filled.",
}, },
"code": {"show": False}, "code": {"show": False},
"inputs": {"field_type": "code"},
} }
def build( def build(
self, self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}
llm: BaseLLM,
prompt: PromptTemplate,
) -> Document: ) -> Document:
chain = prompt | llm chain = prompt | llm
# The input is an empty dict because the prompt is already filled # The input is an empty dict because the prompt is already filled
result = chain.invoke({}) result = chain.invoke(input=inputs)
if hasattr(result, "content"): if hasattr(result, "content"):
result = result.content result = result.content
self.repr_value = result self.repr_value = result

View file

@ -14,7 +14,7 @@ class MetalRetrieverComponent(CustomComponent):
"api_key": {"display_name": "API Key", "password": True}, "api_key": {"display_name": "API Key", "password": True},
"client_id": {"display_name": "Client ID", "password": True}, "client_id": {"display_name": "Client ID", "password": True},
"index_id": {"display_name": "Index ID"}, "index_id": {"display_name": "Index ID"},
"params": {"display_name": "Parameters", "field_type": "code"}, "params": {"display_name": "Parameters"},
"code": {"show": False}, "code": {"show": False},
} }

View file

@ -19,7 +19,6 @@ class GetRequest(CustomComponent):
}, },
"headers": { "headers": {
"display_name": "Headers", "display_name": "Headers",
"field_type": "code",
"info": "The headers to send with the request.", "info": "The headers to send with the request.",
}, },
"code": {"show": False}, "code": {"show": False},

View file

@ -15,7 +15,6 @@ class PostRequest(CustomComponent):
"url": {"display_name": "URL", "info": "The URL to make the request to."}, "url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": { "headers": {
"display_name": "Headers", "display_name": "Headers",
"field_type": "code",
"info": "The headers to send with the request.", "info": "The headers to send with the request.",
}, },
"code": {"show": False}, "code": {"show": False},

View file

@ -15,7 +15,7 @@ class UpdateRequest(CustomComponent):
"url": {"display_name": "URL", "info": "The URL to make the request to."}, "url": {"display_name": "URL", "info": "The URL to make the request to."},
"headers": { "headers": {
"display_name": "Headers", "display_name": "Headers",
"field_type": "code", "field_type": "NestedDict",
"info": "The headers to send with the request.", "info": "The headers to send with the request.",
}, },
"code": {"show": False}, "code": {"show": False},

View file

@ -0,0 +1,3 @@
from .base import NestedDict
__all__ = ["NestedDict"]

View file

@ -0,0 +1,4 @@
from typing import Union, Dict
# Type alias for more complex dicts
NestedDict = Dict[str, Union[str, Dict]]

View file

@ -122,6 +122,19 @@ class Vertex:
except Exception as exc: except Exception as exc:
logger.debug(f"Error parsing code: {exc}") logger.debug(f"Error parsing code: {exc}")
params[key] = value.get("value") 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: else:
params[key] = value.get("value") params[key] = value.get("value")

View file

@ -59,7 +59,7 @@ def get_memory_key(langchain_object):
"history": "chat_history", "history": "chat_history",
} }
# Check if memory_key attribute exists # 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 memory_key = langchain_object.memory.memory_key
return mem_key_dict.get(memory_key) return mem_key_dict.get(memory_key)
else: else:
@ -90,5 +90,4 @@ def update_memory_keys(langchain_object, possible_new_mem_key):
try: try:
setattr(langchain_object.memory, attr, key) setattr(langchain_object.memory, attr, key)
except ValueError as exc: except ValueError as exc:
logger.debug( logger.debug(f"{langchain_object.memory} has no attribute {attr} ({exc})")
f"{langchain_object.memory} has no attribute {attr} ({exc})")

View file

@ -140,13 +140,16 @@ class FrontendNode(BaseModel):
@staticmethod @staticmethod
def handle_dict_type(field: TemplateField, _type: str) -> str: def handle_dict_type(field: TemplateField, _type: str) -> str:
"""Handles 'dict' type by replacing it with 'code' or 'file' based on the field name.""" """Handles 'dict' type by replacing it with 'code' or 'file' based on the field name."""
if "dict" in _type.lower(): if "dict" in _type.lower() and field.name == "dict_":
if field.name == "dict_": field.field_type = "file"
field.field_type = "file" field.suffixes = [".json", ".yaml", ".yml"]
field.suffixes = [".json", ".yaml", ".yml"] field.file_types = ["json", "yaml", "yml"]
field.file_types = ["json", "yaml", "yml"] elif (
else: _type.startswith("Dict")
field.field_type = "code" or _type.startswith("Mapping")
or _type.startswith("dict")
):
field.field_type = "dict"
return _type return _type
@staticmethod @staticmethod
@ -240,20 +243,6 @@ class FrontendNode(BaseModel):
"description", "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 @staticmethod
def set_field_default_value(field: TemplateField, value: dict, key: str) -> None: def set_field_default_value(field: TemplateField, value: dict, key: str) -> None:
"""Sets the field value with the default value if present.""" """Sets the field value with the default value if present."""

View file

@ -170,11 +170,11 @@ class DocumentLoaderFrontNode(FrontendNode):
# add a metadata field of type dict # add a metadata field of type dict
self.template.add_field( self.template.add_field(
TemplateField( TemplateField(
field_type="code", field_type="dict",
required=True, required=True,
show=True, show=True,
name="metadata", name="metadata",
value="{}", value={},
display_name="Metadata", display_name="Metadata",
multiline=False, multiline=False,
) )

View file

@ -89,7 +89,7 @@ class EmbeddingFrontendNode(FrontendNode):
if field.name == "headers": if field.name == "headers":
field.show = False field.show = False
if field.name == "model_kwargs": if field.name == "model_kwargs":
field.field_type = "code" field.field_type = "dict"
field.advanced = True field.advanced = True
field.show = True field.show = True
elif field.name in [ elif field.name in [

View file

@ -153,10 +153,13 @@ class DictCodeFileFormatter(FieldFormatter):
key = field.name key = field.name
value = field.to_dict() value = field.to_dict()
_type = value["type"] _type = value["type"]
if "dict" in _type.lower(): if "dict" in _type.lower() and key == "dict_":
if key == "dict_": field.field_type = "file"
field.field_type = "file" field.suffixes = [".json", ".yaml", ".yml"]
field.suffixes = [".json", ".yaml", ".yml"] field.file_types = ["json", "yaml", "yml"]
field.file_types = ["json", "yaml", "yml"] elif (
else: _type.startswith("Dict")
field.field_type = "code" or _type.startswith("Mapping")
or _type.startswith("dict")
):
field.field_type = "dict"

View file

@ -131,7 +131,7 @@ class LLMFrontendNode(FrontendNode):
if display_name := display_names_dict.get(field.name): if display_name := display_names_dict.get(field.name):
field.display_name = display_name field.display_name = display_name
if field.name == "model_kwargs": if field.name == "model_kwargs":
field.field_type = "code" field.field_type = "dict"
field.advanced = True field.advanced = True
field.show = True field.show = True
elif field.name in [ elif field.name in [

View file

@ -15,6 +15,7 @@ from langflow.template.template.base import Template
class PromptFrontendNode(FrontendNode): class PromptFrontendNode(FrontendNode):
@staticmethod @staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None: def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
# if field.field_type == "StringPromptTemplate" # if field.field_type == "StringPromptTemplate"
# change it to str # change it to str
PROMPT_FIELDS = [ PROMPT_FIELDS = [

View file

@ -21,5 +21,4 @@ class UtilitiesFrontendNode(FrontendNode):
field.field_type = "str" field.field_type = "str"
if isinstance(field.value, dict): if isinstance(field.value, dict):
field.field_type = "code"
field.value = orjson_dumps(field.value) field.value = orjson_dumps(field.value)

View file

@ -56,7 +56,7 @@ class VectorStoreFrontendNode(FrontendNode):
# Add search_kwargs field # Add search_kwargs field
extra_field = TemplateField( extra_field = TemplateField(
name="search_kwargs", name="search_kwargs",
field_type="code", field_type="NestedDict",
required=False, required=False,
placeholder="", placeholder="",
show=True, show=True,

View file

@ -48,4 +48,14 @@ def python_function(text: str) -> str:
return text return text
""" """
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"] DIRECT_TYPES = [
"str",
"bool",
"dict",
"int",
"float",
"Any",
"prompt",
"code",
"NestedDict",
]

View file

@ -276,8 +276,6 @@ def format_dict(
value["password"] = is_password_field(key) value["password"] = is_password_field(key)
value["multiline"] = is_multiline_field(key) value["multiline"] = is_multiline_field(key)
replace_dict_type_with_code(value)
if key == "dict_": if key == "dict_":
set_dict_file_attributes(value) 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: def set_dict_file_attributes(value: Dict[str, Any]) -> None:
""" """
Sets the file attributes for the 'dict_' key. Sets the file attributes for the 'dict_' key.

View file

@ -58,6 +58,7 @@
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-tooltip": "^5.21.1", "react-tooltip": "^5.21.1",
"react18-json-view": "^0.2.3",
"reactflow": "^11.8.3", "reactflow": "^11.8.3",
"rehype-mathjax": "^4.0.3", "rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
@ -8872,6 +8873,14 @@
"react-dom": ">=16.6.0" "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": { "node_modules/reactflow": {
"version": "11.8.3", "version": "11.8.3",
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.8.3.tgz", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.8.3.tgz",

View file

@ -53,6 +53,7 @@
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-tooltip": "^5.21.1", "react-tooltip": "^5.21.1",
"react18-json-view": "^0.2.3",
"reactflow": "^11.8.3", "reactflow": "^11.8.3",
"rehype-mathjax": "^4.0.3", "rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",

View file

@ -3,45 +3,78 @@
@tailwind utilities; @tailwind utilities;
.App { .App {
text-align: center; text-align: center;
} }
.App-logo { .App-logo {
height: 40vmin; height: 40vmin;
pointer-events: none; pointer-events: none;
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
.App-logo { .App-logo {
animation: App-logo-spin infinite 20s linear; animation: App-logo-spin infinite 20s linear;
} }
} }
.App-header { .App-header {
background-color: #282c34; background-color: #282c34;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: white; color: white;
} }
.App-link { .App-link {
color: #61dafb; color: #61dafb;
} }
@keyframes App-logo-spin { @keyframes App-logo-spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@font-face { @font-face {
font-family: text-security-disc; font-family: text-security-disc;
src: url("assets/text-security-disc.woff") format("woff"); 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;
} }

View file

@ -9,6 +9,7 @@ import React, {
import { Handle, Position, useUpdateNodeInternals } from "reactflow"; import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../../../components/codeAreaComponent"; import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
import Dropdown from "../../../../components/dropdownComponent"; import Dropdown from "../../../../components/dropdownComponent";
import FloatComponent from "../../../../components/floatComponent"; import FloatComponent from "../../../../components/floatComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
@ -16,6 +17,7 @@ import InputComponent from "../../../../components/inputComponent";
import InputFileComponent from "../../../../components/inputFileComponent"; import InputFileComponent from "../../../../components/inputFileComponent";
import InputListComponent from "../../../../components/inputListComponent"; import InputListComponent from "../../../../components/inputListComponent";
import IntComponent from "../../../../components/intComponent"; import IntComponent from "../../../../components/intComponent";
import KeypairListComponent from "../../../../components/keypairListComponent";
import PromptAreaComponent from "../../../../components/promptComponent"; import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent"; import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent";
@ -24,7 +26,12 @@ import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components"; import { ParameterComponentType } from "../../../../types/components";
import { TabsState } from "../../../../types/tabs"; import { TabsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils"; import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
} from "../../../../utils/reactflowUtils";
import { import {
nodeColors, nodeColors,
nodeIconsLucide, nodeIconsLucide,
@ -74,13 +81,18 @@ export default function ParameterComponent({
const { data: myData } = useContext(typesContext); const { data: myData } = useContext(typesContext);
const handleOnNewValue = (newValue: string | string[] | boolean): void => { const handleOnNewValue = (
newValue: string | string[] | boolean | Object[]
): void => {
let newData = cloneDeep(data); let newData = cloneDeep(data);
newData.node!.template[name].value = newValue; newData.node!.template[name].value = newValue;
setData(newData); setData(newData);
// Set state to pending // Set state to pending
//@ts-ignore //@ts-ignore
setTabsState((prev: TabsState) => { setTabsState((prev: TabsState) => {
if (!prev[tabId]) {
return prev;
}
return { return {
...prev, ...prev,
[tabId]: { [tabId]: {
@ -93,6 +105,8 @@ export default function ParameterComponent({
renderTooltips(); renderTooltips();
}; };
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => { useEffect(() => {
if (name === "openai_api_base") console.log(info); if (name === "openai_api_base") console.log(info);
// @ts-ignore // @ts-ignore
@ -214,6 +228,8 @@ export default function ParameterComponent({
type === "code" || type === "code" ||
type === "prompt" || type === "prompt" ||
type === "file" || type === "file" ||
type === "dict" ||
type === "NestedDict" ||
type === "int") && type === "int") &&
!optionalHandle ? ( !optionalHandle ? (
<></> <></>
@ -350,6 +366,42 @@ export default function ParameterComponent({
onChange={handleOnNewValue} onChange={handleOnNewValue}
/> />
</div> </div>
) : left === true && type === "NestedDict" ? (
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value ?? {
yourkey: "value",
}
}
onChange={(newValue) => {
data.node!.template[name].value = newValue;
handleOnNewValue(newValue);
}}
/>
</div>
) : left === true && type === "dict" ? (
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
data.node!.template[name].value = valueToNumbers;
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
handleOnNewValue(valueToNumbers);
}}
/>
</div>
) : ( ) : (
<></> <></>
)} )}

View file

@ -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 (
<div
className={classNames(
value.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
)}
>
{
<div className="flex w-full gap-3">
<DictAreaModal
value={value}
onChange={(obj) => {
onChange(obj);
}}
>
<Input
type="text"
className={
editNode
? "input-edit-node input-disable pointer-events-none cursor-pointer"
: "input-disable pointer-events-none cursor-pointer"
}
placeholder="Click to edit your dictionary..."
/>
</DictAreaModal>
</div>
}
</div>
);
}

View file

@ -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 (
<div
className={classNames(
value?.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
)}
>
{value?.map((obj, index) => {
return Object.keys(obj).map((key, idx) => {
return (
<div key={idx} className="flex w-full gap-3">
<Input
type="text"
value={key.trim()}
className={classNames(
editNode ? "input-edit-node" : "",
duplicateKey ? "input-invalid" : ""
)}
placeholder="Type key..."
onChange={(event) => handleChangeKey(event, index)}
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Backspace") {
e.preventDefault();
e.stopPropagation();
}
}}
/>
<Input
type="text"
value={obj[key]}
className={editNode ? "input-edit-node" : ""}
placeholder="Type a value..."
onChange={(event) =>
handleChangeValue(event.target.value, index)
}
/>
{index === value.length - 1 ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(value);
newInputList.push({ "": "" });
onChange(newInputList);
}}
>
<IconComponent
name="Plus"
className={"h-4 w-4 hover:text-accent-foreground"}
/>
</button>
) : (
<button
onClick={() => {
let newInputList = _.cloneDeep(value);
newInputList.splice(index, 1);
onChange(newInputList);
}}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
/>
</button>
)}
</div>
);
});
})}
</div>
);
}

View file

@ -13,7 +13,7 @@ const DialogPortal = ({
...props ...props
}: DialogPrimitive.DialogPortalProps) => ( }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}> <DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center"> <div className="nopan nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children} {children}
</div> </div>
</DialogPrimitive.Portal> </DialogPrimitive.Portal>
@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "nopan nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className
)} )}
{...props} {...props}

View file

@ -115,6 +115,9 @@ export const EDIT_DIALOG_SUBTITLE =
export const CODE_PROMPT_DIALOG_SUBTITLE = export const CODE_PROMPT_DIALOG_SUBTITLE =
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string."; "Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
export const CODE_DICT_DIALOG_SUBTITLE =
"Edit your dictionary. This dialog allows you to create your own customized dictionary. You can add as many key-value pairs as you want. While in edit mode, you can enter ({}) or ([]), and this will result in adding a new object or array.";
/** /**
* The base text for subtitle of Prompt Dialog * The base text for subtitle of Prompt Dialog
* @constant * @constant

View file

@ -1,6 +1,7 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react"; import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
import CodeAreaComponent from "../../components/codeAreaComponent"; import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent"; import Dropdown from "../../components/dropdownComponent";
import FloatComponent from "../../components/floatComponent"; import FloatComponent from "../../components/floatComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
@ -8,6 +9,7 @@ import InputComponent from "../../components/inputComponent";
import InputFileComponent from "../../components/inputFileComponent"; import InputFileComponent from "../../components/inputFileComponent";
import InputListComponent from "../../components/inputListComponent"; import InputListComponent from "../../components/inputListComponent";
import IntComponent from "../../components/intComponent"; import IntComponent from "../../components/intComponent";
import KeypairListComponent from "../../components/keypairListComponent";
import PromptAreaComponent from "../../components/promptComponent"; import PromptAreaComponent from "../../components/promptComponent";
import TextAreaComponent from "../../components/textAreaComponent"; import TextAreaComponent from "../../components/textAreaComponent";
import ToggleShadComponent from "../../components/toggleShadComponent"; import ToggleShadComponent from "../../components/toggleShadComponent";
@ -26,6 +28,10 @@ import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs"; import { TabsState } from "../../types/tabs";
import {
convertObjToArray,
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
@ -78,6 +84,25 @@ const EditNodeModal = forwardRef(
setMyData(data); // reset data to what it is on node when opening modal setMyData(data); // reset data to what it is on node when opening modal
}, [modalOpen]); }, [modalOpen]);
const [obj, setObj] = useState({
arr: ["test", 123456, false, null],
boolean: false,
longString:
"long string long string long string long string long string long string",
number: 123456,
try: {
k1: 123,
k2: "123",
k3: false,
},
string: "string",
});
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const [dictArr, setDictArr] = useState([
{ yourKey: "yourValue" },
] as Object[]);
return ( return (
<BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}> <BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
@ -166,6 +191,52 @@ const EditNodeModal = forwardRef(
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
/> />
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={obj}
onChange={(newValue) => {
setObj(newValue);
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
myData.node.template[templateParam]
.value?.length === 0 ||
!myData.node.template[templateParam]
.value
? dictArr
: convertObjToArray(
myData.node.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
setErrorDuplicateKey(
hasDuplicateKeys(newValue)
);
if (hasDuplicateKeys(newValue)) {
setDictArr(newValue);
} else {
setDictArr(newValue);
myData.node!.template[
templateParam
].value = newValue;
}
}}
/>
</div>
) : myData.node.template[templateParam] ) : myData.node.template[templateParam]
.multiline ? ( .multiline ? (
<TextAreaComponent <TextAreaComponent

View file

@ -0,0 +1,67 @@
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
// import "ace-builds/webpack-resolver";
import { useEffect, useState } from "react";
import JsonView from "react18-json-view";
import "react18-json-view/src/dark.css";
import "react18-json-view/src/style.css";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { CODE_DICT_DIALOG_SUBTITLE } from "../../constants/constants";
import BaseModal from "../baseModal";
export default function DictAreaModal({
children,
onChange,
value,
}): JSX.Element {
const [open, setOpen] = useState(false);
const [dictObj, setDictObj] = useState(value);
useEffect(() => {
if (value) setDictObj(value);
}, [value]);
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_DICT_DIALOG_SUBTITLE}>
<span className="pr-2">Edit Dictionary</span>
<IconComponent
name="BookMarked"
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex h-full w-full flex-col transition-all">
<JsonView
theme="vscode"
dark={true}
editable
enableClipboard
onEdit={(edit) => {
setDictObj(edit["src"]);
}}
src={dictObj}
/>
<div className="flex h-fit w-full justify-end">
<Button
className="mt-3"
type="submit"
onClick={() => {
onChange(dictObj);
setOpen(false);
}}
>
Save
</Button>
</div>
</div>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -1057,4 +1057,8 @@
.label-invalid{ .label-invalid{
@apply text-destructive @apply text-destructive
} }
.input-invalid{
@apply border-destructive focus:ring-destructive focus:border-destructive
}
} }

View file

@ -54,6 +54,21 @@ export type InputListComponentType = {
editNode?: boolean; 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 = { export type TextAreaComponentType = {
field_name?: string; field_name?: string;
nodeClass?: APIClassType; nodeClass?: APIClassType;

View file

@ -204,30 +204,49 @@ export function validateNode(
node: { template }, node: { template },
} = node.data; } = node.data;
return Object.keys(template).reduce( return Object.keys(template).reduce((errors: Array<string>, t) => {
(errors: Array<string>, t) => if (
errors.concat( template[t].required &&
template[t].required && template[t].show &&
template[t].show && (template[t].value === undefined ||
(template[t].value === undefined || template[t].value === null ||
template[t].value === null || template[t].value === "") &&
template[t].value === "") && !reactFlowInstance
!reactFlowInstance .getEdges()
.getEdges() .some(
.some( (edge) =>
(edge) => edge.targetHandle?.split("|")[1] === t &&
edge.targetHandle?.split("|")[1] === t && edge.targetHandle.split("|")[2] === node.id
edge.targetHandle.split("|")[2] === node.id )
) ) {
? [ errors.push(
`${type} is missing ${ `${type} is missing ${
template.display_name || toNormalCase(template[t].name) template.display_name || toNormalCase(template[t].name)
}.`, }.`
] );
: [] } else if (
), template[t].type === "dict" &&
[] as string[] 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) { export function validateNodes(reactFlowInstance: ReactFlowInstance) {
@ -291,3 +310,58 @@ export function getConnectedNodes(
const targetId = edge.target; const targetId = edge.target;
return nodes.filter((node) => node.id === targetId || node.id === sourceId); 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;
});
}

View file

@ -1,6 +1,7 @@
import { import {
ArrowUpToLine, ArrowUpToLine,
Bell, Bell,
BookMarked,
Check, Check,
CheckCircle2, CheckCircle2,
ChevronDown, ChevronDown,
@ -307,5 +308,6 @@ export const nodeIconsLucide: iconsType = {
UserCog2, UserCog2,
Key, Key,
Unplug, Unplug,
BookMarked,
ChevronUp, ChevronUp,
}; };

View file

@ -1,97 +1,4 @@
from fastapi.testclient import TestClient 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): def test_openai(client: TestClient, logged_in_headers):
@ -270,7 +177,7 @@ def test_openai(client: TestClient, logged_in_headers):
"multiline": False, "multiline": False,
"password": False, "password": False,
"name": "model_kwargs", "name": "model_kwargs",
"type": "code", "type": "dict",
"list": False, "list": False,
"advanced": True, "advanced": True,
"info": "", "info": "",
@ -325,7 +232,7 @@ def test_openai(client: TestClient, logged_in_headers):
"multiline": False, "multiline": False,
"password": False, "password": False,
"name": "logit_bias", "name": "logit_bias",
"type": "code", "type": "dict",
"list": False, "list": False,
"advanced": False, "advanced": False,
"info": "", "info": "",
@ -442,7 +349,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers):
"multiline": False, "multiline": False,
"password": False, "password": False,
"name": "model_kwargs", "name": "model_kwargs",
"type": "code", "type": "dict",
"list": False, "list": False,
"advanced": True, "advanced": True,
"info": "", "info": "",

View file

@ -55,7 +55,7 @@ def test_prompt_template(client: TestClient, logged_in_headers):
"multiline": False, "multiline": False,
"password": False, "password": False,
"name": "partial_variables", "name": "partial_variables",
"type": "code", "type": "dict",
"list": False, "list": False,
"advanced": False, "advanced": False,
"info": "", "info": "",

View file

@ -135,7 +135,7 @@ def test_format_dict():
} }
expected_output = { expected_output = {
"field1": { "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, "required": False,
"list": False, "list": False,
"show": False, "show": False,
@ -249,7 +249,7 @@ def test_format_dict():
} }
expected_output = { expected_output = {
"field1": { "field1": {
"type": "code", "type": "Dict[str, int]",
"required": False, "required": False,
"list": False, "list": False,
"show": False, "show": False,