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 |
| --------------------------------------------------------- |
| _`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.
<Admonition type="info">
Unlike Langchain types, base Python types do not add a
[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.",
},
"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

View file

@ -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},
}

View file

@ -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},

View file

@ -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},

View file

@ -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},

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:
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")

View file

@ -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})")

View file

@ -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."""

View file

@ -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,
)

View file

@ -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 [

View file

@ -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"

View file

@ -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 [

View file

@ -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 = [

View file

@ -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)

View file

@ -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,

View file

@ -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",
]

View file

@ -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.

View file

@ -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",

View file

@ -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",

View file

@ -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;
}

View file

@ -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}
/>
</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
}: DialogPrimitive.DialogPortalProps) => (
<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}
</div>
</DialogPrimitive.Portal>
@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
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
)}
{...props}

View file

@ -115,6 +115,9 @@ export const EDIT_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.";
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
* @constant

View file

@ -1,6 +1,7 @@
import { cloneDeep } from "lodash";
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
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";
@ -8,6 +9,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";
@ -26,6 +28,10 @@ import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs";
import {
convertObjToArray,
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
@ -78,6 +84,25 @@ const EditNodeModal = forwardRef(
setMyData(data); // reset data to what it is on node when opening modal
}, [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 (
<BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
@ -166,6 +191,52 @@ const EditNodeModal = forwardRef(
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]
.multiline ? (
<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{
@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;
};
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;

View file

@ -204,30 +204,49 @@ export function validateNode(
node: { template },
} = node.data;
return Object.keys(template).reduce(
(errors: Array<string>, 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<string>, 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;
});
}

View file

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

View file

@ -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": "",

View file

@ -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": "",

View file

@ -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,