Add RangeSpec class and functionality in the UI (#1192)
This commit is contained in:
commit
2703dd06d3
12 changed files with 88 additions and 8 deletions
|
|
@ -73,7 +73,7 @@ The CustomComponent class serves as the foundation for creating custom component
|
||||||
| _`required: bool`_ | Makes the field required. |
|
| _`required: bool`_ | Makes the field required. |
|
||||||
| _`info: str`_ | Adds a tooltip to the field. |
|
| _`info: str`_ | Adds a tooltip to the field. |
|
||||||
| _`file_types: List[str]`_ | This is a requirement if the _`field_type`_ is _file_. Defines which file types will be accepted. For example, _json_, _yaml_ or _yml_. |
|
| _`file_types: List[str]`_ | This is a requirement if the _`field_type`_ is _file_. Defines which file types will be accepted. For example, _json_, _yaml_ or _yml_. |
|
||||||
|
| _`range_spec: langflow.field_typing.RangeSpec`_ | This is a requirement if the _`field_type`_ is _`float`_. Defines the range of values accepted and the step size. If none is defined, the default is _`[-1, 1, 0.1]`_. |
|
||||||
- The CustomComponent class also provides helpful methods for specific tasks (e.g., to load and use other flows from the Langflow platform):
|
- The CustomComponent class also provides helpful methods for specific tasks (e.g., to load and use other flows from the Langflow platform):
|
||||||
|
|
||||||
| Method Name | Description |
|
| Method Name | Description |
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from .constants import (
|
||||||
Tool,
|
Tool,
|
||||||
VectorStore,
|
VectorStore,
|
||||||
)
|
)
|
||||||
|
from .range_spec import RangeSpec
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"NestedDict",
|
"NestedDict",
|
||||||
|
|
@ -48,5 +49,6 @@ __all__ = [
|
||||||
"BasePromptTemplate",
|
"BasePromptTemplate",
|
||||||
"ChatPromptTemplate",
|
"ChatPromptTemplate",
|
||||||
"Prompt",
|
"Prompt",
|
||||||
|
"RangeSpec",
|
||||||
"TemplateField",
|
"TemplateField",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
21
src/backend/langflow/field_typing/range_spec.py
Normal file
21
src/backend/langflow/field_typing/range_spec.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from pydantic import BaseModel, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class RangeSpec(BaseModel):
|
||||||
|
min: float = -1.0
|
||||||
|
max: float = 1.0
|
||||||
|
step: float = 0.1
|
||||||
|
|
||||||
|
@field_validator("max")
|
||||||
|
@classmethod
|
||||||
|
def max_must_be_greater_than_min(cls, v, values, **kwargs):
|
||||||
|
if "min" in values.data and v <= values.data["min"]:
|
||||||
|
raise ValueError("max must be greater than min")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator("step")
|
||||||
|
@classmethod
|
||||||
|
def step_must_be_positive(cls, v):
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("step must be positive")
|
||||||
|
return v
|
||||||
|
|
@ -8,6 +8,9 @@ from uuid import UUID
|
||||||
|
|
||||||
from cachetools import LRUCache, cached
|
from cachetools import LRUCache, cached
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from langflow.field_typing.range_spec import RangeSpec
|
||||||
from langflow.interface.agents.base import agent_creator
|
from langflow.interface.agents.base import agent_creator
|
||||||
from langflow.interface.chains.base import chain_creator
|
from langflow.interface.chains.base import chain_creator
|
||||||
from langflow.interface.custom.custom_component import CustomComponent
|
from langflow.interface.custom.custom_component import CustomComponent
|
||||||
|
|
@ -31,7 +34,6 @@ from langflow.template.field.base import TemplateField
|
||||||
from langflow.template.frontend_node.constants import CLASSES_TO_REMOVE
|
from langflow.template.frontend_node.constants import CLASSES_TO_REMOVE
|
||||||
from langflow.template.frontend_node.custom_components import CustomComponentFrontendNode
|
from langflow.template.frontend_node.custom_components import CustomComponentFrontendNode
|
||||||
from langflow.utils.util import get_base_classes
|
from langflow.utils.util import get_base_classes
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
# Used to get the base_classes list
|
# Used to get the base_classes list
|
||||||
|
|
@ -272,6 +274,10 @@ def update_field_dict(field_dict):
|
||||||
field_dict["value"] = field_dict["value"](field_dict.get("options", []))
|
field_dict["value"] = field_dict["value"](field_dict.get("options", []))
|
||||||
field_dict["refresh"] = True
|
field_dict["refresh"] = True
|
||||||
|
|
||||||
|
# Let's check if "range_spec" is a RangeSpec object
|
||||||
|
if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec):
|
||||||
|
field_dict["rangeSpec"] = field_dict["rangeSpec"].model_dump()
|
||||||
|
|
||||||
|
|
||||||
def add_extra_fields(frontend_node, field_config, function_args):
|
def add_extra_fields(frontend_node, field_config, function_args):
|
||||||
"""Add extra fields to the frontend node"""
|
"""Add extra fields to the frontend node"""
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ from typing import Any, Callable, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
||||||
|
|
||||||
|
from langflow.field_typing.range_spec import RangeSpec
|
||||||
|
|
||||||
|
|
||||||
class TemplateField(BaseModel):
|
class TemplateField(BaseModel):
|
||||||
model_config = ConfigDict()
|
model_config = ConfigDict()
|
||||||
|
|
@ -60,6 +62,9 @@ class TemplateField(BaseModel):
|
||||||
refresh: Optional[bool] = None
|
refresh: Optional[bool] = None
|
||||||
"""Specifies if the field should be refreshed. Defaults to False."""
|
"""Specifies if the field should be refreshed. Defaults to False."""
|
||||||
|
|
||||||
|
range_spec: Optional[RangeSpec] = Field(None, serialization_alias="rangeSpec")
|
||||||
|
"""Range specification for the field. Defaults to None."""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return self.model_dump(by_alias=True, exclude_none=True)
|
return self.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
|
@ -68,3 +73,11 @@ class TemplateField(BaseModel):
|
||||||
if self.field_type == "file":
|
if self.field_type == "file":
|
||||||
return value
|
return value
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@field_serializer("field_type")
|
||||||
|
def serialize_field_type(self, value, _info):
|
||||||
|
if value == "float":
|
||||||
|
# check if range_spec is set
|
||||||
|
if self.range_spec is None:
|
||||||
|
self.range_spec = RangeSpec()
|
||||||
|
return value
|
||||||
|
|
|
||||||
|
|
@ -457,6 +457,7 @@ export default function ParameterComponent({
|
||||||
<FloatComponent
|
<FloatComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node?.template[name].value ?? ""}
|
value={data.node?.template[name].value ?? ""}
|
||||||
|
rangeSpec={data.node?.template[name].rangeSpec}
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,11 @@ export default function CodeTabsComponent({
|
||||||
templateField
|
templateField
|
||||||
].value
|
].value
|
||||||
}
|
}
|
||||||
|
rangeSpec={
|
||||||
|
node.data.node.template[
|
||||||
|
templateField
|
||||||
|
].rangeSpec
|
||||||
|
}
|
||||||
onChange={(target) => {
|
onChange={(target) => {
|
||||||
setData((old) => {
|
setData((old) => {
|
||||||
let newInputList =
|
let newInputList =
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@ export default function FloatComponent({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
|
rangeSpec,
|
||||||
editNode = false,
|
editNode = false,
|
||||||
}: FloatComponentType): JSX.Element {
|
}: FloatComponentType): JSX.Element {
|
||||||
const step = 0.1;
|
const step = rangeSpec?.step ?? 0.1;
|
||||||
const min = -2;
|
const min = rangeSpec?.min ?? -2;
|
||||||
const max = 2;
|
const max = rangeSpec?.max ?? 2;
|
||||||
|
console.log("FloatComponent", value, disabled, rangeSpec, editNode);
|
||||||
|
console.log("FloatComponent", step, min, max);
|
||||||
|
|
||||||
// Clear component state
|
// Clear component state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -40,7 +43,9 @@ export default function FloatComponent({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={editNode ? "input-edit-node" : ""}
|
className={editNode ? "input-edit-node" : ""}
|
||||||
placeholder={
|
placeholder={
|
||||||
editNode ? "Number -2 to 2" : "Type a number from minus two to two"
|
editNode
|
||||||
|
? `Enter a value between ${min} and ${max}`
|
||||||
|
: `Enter a value between ${min} and ${max}`
|
||||||
}
|
}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange(event.target.value);
|
onChange(event.target.value);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { FloatComponentType } from "../../types/components";
|
import { IntComponentType } from "../../types/components";
|
||||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function IntComponent({
|
||||||
disabled,
|
disabled,
|
||||||
editNode = false,
|
editNode = false,
|
||||||
id = "",
|
id = "",
|
||||||
}: FloatComponentType): JSX.Element {
|
}: IntComponentType): JSX.Element {
|
||||||
const min = 0;
|
const min = 0;
|
||||||
|
|
||||||
// Clear component state
|
// Clear component state
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,10 @@ const EditNodeModal = forwardRef(
|
||||||
<FloatComponent
|
<FloatComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
|
rangeSpec={
|
||||||
|
myData.node!.template[templateParam]
|
||||||
|
.rangeSpec
|
||||||
|
}
|
||||||
value={
|
value={
|
||||||
myData.node.template[templateParam]
|
myData.node.template[templateParam]
|
||||||
.value ?? ""
|
.value ?? ""
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,26 @@ export type DisclosureComponentType = {
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RangeSpecType = {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IntComponentType = {
|
||||||
|
value: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
editNode?: boolean;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FloatComponentType = {
|
export type FloatComponentType = {
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
rangeSpec: RangeSpecType;
|
||||||
editNode?: boolean;
|
editNode?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["max_tokens"] == {
|
assert template["max_tokens"] == {
|
||||||
|
|
@ -118,6 +119,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["frequency_penalty"] == {
|
assert template["frequency_penalty"] == {
|
||||||
|
|
@ -133,6 +135,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["presence_penalty"] == {
|
assert template["presence_penalty"] == {
|
||||||
|
|
@ -148,6 +151,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["n"] == {
|
assert template["n"] == {
|
||||||
|
|
@ -237,6 +241,7 @@ def test_openai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["logit_bias"] == {
|
assert template["logit_bias"] == {
|
||||||
|
|
@ -359,6 +364,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["model_kwargs"] == {
|
assert template["model_kwargs"] == {
|
||||||
|
|
@ -403,6 +409,7 @@ def test_chat_open_ai(client: TestClient, logged_in_headers):
|
||||||
"list": False,
|
"list": False,
|
||||||
"advanced": False,
|
"advanced": False,
|
||||||
"info": "",
|
"info": "",
|
||||||
|
"rangeSpec": {"max": 1.0, "min": -1.0, "step": 0.1},
|
||||||
"fileTypes": [],
|
"fileTypes": [],
|
||||||
}
|
}
|
||||||
assert template["max_retries"] == {
|
assert template["max_retries"] == {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue