Adding Support for Key-Value Pair Objects in Input Flows (#845)
This commit is contained in:
commit
311b0a57e9
37 changed files with 619 additions and 207 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
3
src/backend/langflow/field_typing/__init__.py
Normal file
3
src/backend/langflow/field_typing/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .base import NestedDict
|
||||
|
||||
__all__ = ["NestedDict"]
|
||||
4
src/backend/langflow/field_typing/base.py
Normal file
4
src/backend/langflow/field_typing/base.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from typing import Union, Dict
|
||||
|
||||
# Type alias for more complex dicts
|
||||
NestedDict = Dict[str, Union[str, Dict]]
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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})")
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
9
src/frontend/package-lock.json
generated
9
src/frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
|
|
|||
53
src/frontend/src/components/dictComponent/index.tsx
Normal file
53
src/frontend/src/components/dictComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
112
src/frontend/src/components/keypairListComponent/index.tsx
Normal file
112
src/frontend/src/components/keypairListComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
67
src/frontend/src/modals/dictAreaModal/index.tsx
Normal file
67
src/frontend/src/modals/dictAreaModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1057,4 +1057,8 @@
|
|||
.label-invalid{
|
||||
@apply text-destructive
|
||||
}
|
||||
|
||||
.input-invalid{
|
||||
@apply border-destructive focus:ring-destructive focus:border-destructive
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
ArrowUpToLine,
|
||||
Bell,
|
||||
BookMarked,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
|
|
@ -307,5 +308,6 @@ export const nodeIconsLucide: iconsType = {
|
|||
UserCog2,
|
||||
Key,
|
||||
Unplug,
|
||||
BookMarked,
|
||||
ChevronUp,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue