Merge branch 'two_edges' into two_edges_dev

This commit is contained in:
italojohnny 2024-06-14 09:29:50 -03:00
commit 902e616286
22 changed files with 235 additions and 186 deletions

View file

@ -5,6 +5,8 @@ from langflow.inputs import StrInput
from langflow.schema import Data
from langflow.template import Output
import re
class URLComponent(Component):
display_name = "URL"
@ -25,8 +27,39 @@ class URLComponent(Component):
Output(display_name="Data", name="data", method="fetch_content"),
]
def ensure_url(self, string: str) -> str:
"""
Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.
Raises an error if the string is not a valid URL.
Parameters:
string (str): The string to be checked and possibly modified.
Returns:
str: The modified string that is ensured to be a URL.
Raises:
ValueError: If the string is not a valid URL.
"""
if not string.startswith(("http://", "https://")):
string = "http://" + string
# Basic URL validation regex
url_regex = re.compile(
r"^(http://|https://)?" # http:// or https://
r"(([a-zA-Z0-9\.-]+)" # domain
r"(\.[a-zA-Z]{2,}))" # top-level domain
r"(:[0-9]{1,5})?" # optional port
r"(\/.*)?$" # optional path
)
if not re.match(url_regex, string):
raise ValueError(f"Invalid URL: {string}")
return string
def fetch_content(self) -> Data:
urls = [url.strip() for url in self.urls if url.strip()]
urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]
loader = WebBaseLoader(web_paths=urls)
docs = loader.load()
data = [Data(content=doc.page_content, **doc.metadata) for doc in docs]

View file

@ -1,90 +1,92 @@
from typing import Union
from langflow.custom import Component
from langflow.field_typing import Text
from langflow.schema import Data
from langflow.template import Input, Output
from langflow.inputs import BoolInput, DropdownInput, StrInput
from langflow.template import Output
class TextOperatorComponent(Component):
display_name = "Text Operator"
description = "Compares two text inputs based on a specified condition such as equality or inequality, with optional case sensitivity."
icon = "equal"
inputs = [
Input(name="input_text", type=str, display_name="Input Text", info="The primary text input for the operation."),
Input(name="match_text", type=str, display_name="Match Text", info="The text input to compare against."),
Input(
name="operator",
type=str,
display_name="Operator",
info="The operator to apply for comparing the texts.",
options=["equals", "not equals", "contains", "starts with", "ends with", "exists"],
StrInput(
name="input_text",
display_name="Input Text",
info="The primary text input for the operation.",
),
Input(
StrInput(
name="match_text",
display_name="Match Text",
info="The text input to compare against.",
),
DropdownInput(
name="operator",
display_name="Operator",
options=["equals", "not equals", "contains", "starts with", "ends with"],
info="The operator to apply for comparing the texts.",
),
BoolInput(
name="case_sensitive",
type=bool,
display_name="Case Sensitive",
info="If true, the comparison will be case sensitive.",
default=False,
value=False,
advanced=True,
),
Input(
StrInput(
name="true_output",
type=Union[str, Data],
display_name="True Output",
info="The output to return or display when the comparison is true.",
input_types=["Text", "Data"],
advanced=True,
),
Input(
StrInput(
name="false_output",
type=Union[str, Data],
display_name="False Output",
info="The output to return or display when the comparison is false.",
input_types=["Text", "Data"],
advanced=True,
),
]
outputs = [
Output(display_name="True Result", name="true_result", method="result_response"),
Output(display_name="False Result", name="false_result", method="result_response"),
Output(display_name="True Result", name="true_result", method="true_response"),
Output(display_name="False Result", name="false_result", method="false_response"),
]
def true_response(self) -> Union[Text, Data]:
self.stop("False Result")
return self.true_output if self.true_output else self.input_text
def false_response(self) -> Union[Text, Data]:
self.stop("True Result")
return self.false_output if self.false_output else self.input_text
def result_response(self) -> Union[Text, Data]:
input_text = self.input_text
match_text = self.match_text
operator = self.operator
case_sensitive = self.case_sensitive
if not input_text or not match_text:
raise ValueError("Both 'input_text' and 'match_text' must be provided and non-empty.")
def evaluate_condition(self, input_text: str, match_text: str, operator: str, case_sensitive: bool) -> bool:
if not case_sensitive:
input_text = input_text.lower()
match_text = match_text.lower()
result = False
if operator == "equals":
result = input_text == match_text
return input_text == match_text
elif operator == "not equals":
result = input_text != match_text
return input_text != match_text
elif operator == "contains":
result = match_text in input_text
return match_text in input_text
elif operator == "starts with":
result = input_text.startswith(match_text)
return input_text.startswith(match_text)
elif operator == "ends with":
result = input_text.endswith(match_text)
return input_text.endswith(match_text)
return False
def true_response(self) -> Text:
result = self.evaluate_condition(self.input_text, self.match_text, self.operator, self.case_sensitive)
if result:
response = self.true_response()
self.stop("false_result")
response = self.true_output if self.true_output else self.input_text
self.status = response
return response
else:
response = self.false_response()
self.stop("true_result")
return ""
def false_response(self) -> Text:
result = self.evaluate_condition(self.input_text, self.match_text, self.operator, self.case_sensitive)
if not result:
self.stop("true_result")
response = self.false_output if self.false_output else self.input_text
self.status = response
return response
else:
self.stop("false_result")
return ""

View file

@ -1,29 +1,38 @@
from langflow.custom import CustomComponent
from langflow.custom import Component
from langflow.field_typing import Text
from langflow.inputs import StrInput
from langflow.template import Output
class CombineTextComponent(CustomComponent):
class CombineTextComponent(Component):
display_name = "Combine Text"
description = "Concatenate two text sources into a single text chunk using a specified delimiter."
icon = "merge"
def build_config(self):
return {
"text1": {
"display_name": "First Text",
"info": "The first text input to concatenate.",
},
"text2": {
"display_name": "Second Text",
"info": "The second text input to concatenate.",
},
"delimiter": {
"display_name": "Delimiter",
"info": "A string used to separate the two text inputs. Defaults to a whitespace.",
},
}
inputs = [
StrInput(
name="text1",
display_name="First Text",
info="The first text input to concatenate.",
),
StrInput(
name="text2",
display_name="Second Text",
info="The second text input to concatenate.",
),
StrInput(
name="delimiter",
display_name="Delimiter",
info="A string used to separate the two text inputs. Defaults to a whitespace.",
default=" ",
),
]
def build(self, text1: str, text2: str, delimiter: str = " ") -> Text:
combined = delimiter.join([text1, text2])
outputs = [
Output(display_name="Combined Text", name="combined_text", method="combine_texts"),
]
def combine_texts(self) -> Text:
combined = self.delimiter.join([self.text1, self.text2])
self.status = combined
return combined

View file

@ -1,25 +0,0 @@
from langflow.custom import CustomComponent
from langflow.field_typing import Text
class CombineTextsUnsortedComponent(CustomComponent):
display_name = "Combine Texts (Unsorted)"
description = "Concatenate text sources into a single text chunk using a specified delimiter."
icon = "merge"
def build_config(self):
return {
"texts": {
"display_name": "Texts",
"info": "The first text input to concatenate.",
},
"delimiter": {
"display_name": "Delimiter",
"info": "A string used to separate the two text inputs. Defaults to a whitespace.",
},
}
def build(self, texts: list[str], delimiter: str = " ") -> Text:
combined = delimiter.join(texts)
self.status = combined
return combined

View file

@ -1,36 +0,0 @@
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.helpers.data import data_to_text
from langflow.schema import Data
class DataToTextComponent(CustomComponent):
display_name = "Data To Text"
description = "Convert Data into plain text following a specified template."
def build_config(self):
return {
"data": {
"display_name": "Data",
"info": "The data to convert to text.",
},
"template": {
"display_name": "Template",
"info": "The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
"multiline": True,
},
}
def build(
self,
data: list[Data],
template: str = "Text: {text}\nData: {data}",
) -> Text:
if not data:
return ""
if isinstance(data, Data):
data = [data]
result_string = data_to_text(template, data)
self.status = result_string
return result_string

View file

@ -6,9 +6,10 @@ from langflow.custom import CustomComponent
from langflow.schema import Data
class DocumentToDataComponent(CustomComponent):
display_name = "Documents To Data"
class DocumentsToDataComponent(CustomComponent):
display_name = "Documents Data"
description = "Convert LangChain Documents into Data."
icon = "LangChain"
field_config = {
"documents": {"display_name": "Documents"},

View file

@ -1,20 +1,28 @@
from typing import List
from langflow.custom import Component
from langflow.inputs import StrInput
from langflow.inputs import StrInput, HandleInput
from langflow.schema import Data
from langflow.template import Input, Output
from langflow.template import Output
class FilterDataComponent(Component):
display_name = "Filter Message"
description = "Filters a Message object based on a list of strings."
display_name = "Filter Data"
description = "Filters a Data object based on a list of keys."
icon = "filter"
inputs = [
Input(name="message", display_name="Message", info="Message object to filter.", input_types=["Message"]),
HandleInput(
name="data",
display_name="Data",
info="Data object to filter.",
input_types=["Message", "Data"],
),
StrInput(
name="filter_criteria", display_name="Filter Criteria", info="List of strings to filter by.", is_list=True
name="filter_criteria",
display_name="Filter Criteria",
info="List of keys to filter by.",
is_list=True,
),
]
@ -24,10 +32,12 @@ class FilterDataComponent(Component):
def filter_data(self) -> Data:
filter_criteria: List[str] = self.filter_criteria
data = self.data.data if isinstance(self.data, Data) else {}
# Filter the data
filtered = {key: value for key, value in self.message.data.items() if key == filter_criteria}
filtered = {key: value for key, value in data.items() if key in filter_criteria}
# Create a new Data object with the filtered data
self.status = filtered
return filtered
filtered_data = Data(data=filtered)
self.status = filtered_data
return filtered_data

View file

@ -0,0 +1,34 @@
from langflow.custom import Component
from langflow.helpers.data import data_to_text
from langflow.field_typing import Text
from langflow.inputs import MultilineInput, HandleInput
from langflow.template import Output
class ParseDataComponent(Component):
display_name = "Parse Data"
description = "Convert Data into plain text following a specified template."
icon = "braces"
inputs = [
HandleInput(
name="data", display_name="Data", info="The data to convert to text.", input_types=["Message", "Data"]
),
MultilineInput(
name="template",
display_name="Template",
info="The template to use for formatting the data. It can contain the keys {text}, {data} or any other key in the Data.",
),
]
outputs = [
Output(display_name="Text", name="text", method="parse_data_to_text"),
]
def parse_data_to_text(self) -> Text:
data = self.data if isinstance(self.data, list) else [self.data]
template = self.template or "Text: {text}"
result_string = data_to_text(template, data)
self.status = result_string
return result_string

View file

@ -1,15 +1,15 @@
from .CreateData import CreateDataComponent
from .CustomComponent import Component
from .DataToText import DataToTextComponent
from .DocumentToData import DocumentToDataComponent
from .ParseData import ParseDataComponent
from .DocumentToData import DocumentsToDataComponent
from .IDGenerator import UUIDGeneratorComponent
from .UpdateData import UpdateDataComponent
__all__ = [
"Component",
"UpdateDataComponent",
"DocumentToDataComponent",
"DocumentsToDataComponent",
"UUIDGeneratorComponent",
"DataToTextComponent",
"ParseDataComponent",
"CreateDataComponent",
]

View file

@ -54,7 +54,7 @@ class ChatAntropicSpecsComponent(CustomComponent):
self,
model: str,
anthropic_api_key: Optional[str] = None,
max_tokens: Optional[int] = None,
max_tokens: Optional[int] = 1000,
temperature: Optional[float] = None,
api_endpoint: Optional[str] = None,
) -> BaseLanguageModel:

View file

@ -66,7 +66,7 @@ class AnthropicLLM(CustomComponent):
self,
model: str,
anthropic_api_key: Optional[str] = None,
max_tokens: Optional[int] = None,
max_tokens: Optional[int] = 1000,
temperature: Optional[float] = None,
anthropic_api_url: Optional[str] = None,
) -> BaseLanguageModel:

View file

@ -82,7 +82,7 @@ class AnthropicLLM(LCModelComponent):
input_value: Text,
system_message: Optional[str] = None,
anthropic_api_key: Optional[str] = None,
max_tokens: Optional[int] = None,
max_tokens: Optional[int] = 1000,
temperature: Optional[float] = None,
anthropic_api_url: Optional[str] = None,
stream: bool = False,

View file

@ -98,10 +98,15 @@ class Component(CustomComponent):
raw = self.status
if hasattr(raw, "data") and raw is not None:
raw = raw.data
if raw is None:
raw = custom_repr
elif hasattr(raw, "model_dump") and raw is not None:
raw = raw.model_dump()
artifact_type = get_artifact_type(self.status, result)
if raw is None and isinstance(result, (dict, Data, str)):
raw = result.data if isinstance(result, Data) else result
artifact_type = get_artifact_type(self.repr_value or raw, result)
raw = post_process_raw(raw, artifact_type)
artifact = {"repr": custom_repr, "raw": raw, "type": artifact_type}
_artifacts[output.name] = artifact
@ -110,21 +115,15 @@ class Component(CustomComponent):
return _results, _artifacts
def custom_repr(self):
# ! Temporary REPR
# Since all are dict, yaml.dump them
if isinstance(self._results, dict):
_build_results = recursive_serialize_or_str(self._results)
try:
custom_repr = yaml.dump(_build_results)
except Exception as e:
logger.error(f"Error while dumping build_result: {e}")
custom_repr = str(self._results)
if custom_repr is None and isinstance(self._results, (dict, Data, str)):
custom_repr = self._results
if not isinstance(custom_repr, str):
custom_repr = str(custom_repr)
return custom_repr
if self.repr_value == "":
self.repr_value = self.status
if isinstance(self.repr_value, dict):
return yaml.dump(self.repr_value)
if isinstance(self.repr_value, str):
return self.repr_value
if isinstance(self.repr_value, BaseModel) and not isinstance(self.repr_value, Data):
return str(self.repr_value)
return self.repr_value
def build_inputs(self, user_id: Optional[Union[str, UUID]] = None):
"""

View file

@ -35,8 +35,8 @@ class ResultData(BaseModel):
message = values["artifacts"][key]
# ! Temporary fix
if not isinstance(message, dict):
message = {"message": message}
if message is None:
continue
if "stream_url" in message and "type" in message:
stream_url = StreamURL(location=message["stream_url"])

View file

@ -234,16 +234,15 @@ class Vertex:
self.has_session_id = "session_id" in template_dicts
self.required_inputs = [
template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"]
]
self.optional_inputs = [
template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"]
]
# Add the template_dicts[key]["input_types"] to the optional_inputs
self.optional_inputs.extend(
[input_type for value in template_dicts.values() for input_type in value.get("input_types", [])]
)
self.required_inputs = []
self.optional_inputs = []
for value_dict in template_dicts.values():
list_to_append = self.required_inputs if value_dict.get("required") else self.optional_inputs
if "type" in value_dict:
list_to_append.append(value_dict["type"])
if "input_types" in value_dict:
list_to_append.extend(value_dict["input_types"])
template_dict = self.data["node"]["template"]
self.vertex_type = (

View file

@ -5,11 +5,12 @@ from .inputs import (
FileInput,
FloatInput,
IntInput,
MultilineInput,
NestedDictInput,
PromptInput,
SecretStrInput,
StrInput,
MultilineInput,
HandleInput,
)
__all__ = [
@ -24,4 +25,5 @@ __all__ = [
"FileInput",
"PromptInput",
"MultilineInput",
"HandleInput",
]

View file

@ -25,7 +25,7 @@ SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value
class BaseInputMixin(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
field_type: Optional[SerializableFieldTypes] = Field(default=FieldTypes.TEXT)
field_type: Optional[SerializableFieldTypes | str] = Field(default=FieldTypes.TEXT)
required: bool = False
"""Specifies if the field is required. Defaults to False."""

View file

@ -1,6 +1,6 @@
from typing import Callable, Optional, Union
from pydantic import Field
from pydantic import Field, model_validator
from langflow.inputs.validators import StrictBoolean
@ -16,6 +16,17 @@ from .input_mixin import (
)
class HandleInput(BaseInputMixin):
input_types: list[str] = Field(default_factory=list)
field_type: Optional[str] = ""
@model_validator(mode="after")
def validate_model_type(self):
# FieldType should be a string
self.field_type = " | ".join(self.input_types)
return self
class PromptInput(BaseInputMixin, ListableInputMixin):
field_type: Optional[SerializableFieldTypes] = FieldTypes.PROMPT
@ -24,6 +35,7 @@ class PromptInput(BaseInputMixin, ListableInputMixin):
class StrInput(BaseInputMixin, ListableInputMixin, DatabaseLoadMixin): # noqa: F821
field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT
load_from_db: StrictBoolean = False
input_types: list[str] = ["Text"]
"""Defines if the field will allow the user to open a text editor. Default is False."""
@ -83,4 +95,5 @@ InputTypes = Union[
FileInput,
PromptInput,
MultilineInput,
HandleInput,
]

View file

@ -23,7 +23,9 @@ class Template(BaseModel):
# first sort alphabetically
# then sort fields so that fields that have .field_type in DIRECT_TYPES are first
self.fields.sort(key=lambda x: x.name)
self.fields.sort(key=lambda x: x.field_type in DIRECT_TYPES, reverse=False)
self.fields.sort(
key=lambda x: x.field_type in DIRECT_TYPES if hasattr(x, "field_type") else False, reverse=False
)
@model_serializer(mode="wrap")
def serialize_model(self, handler):

View file

@ -302,6 +302,12 @@ export default function GenericNode({
);
};
useEffect(() => {
if (hiddenOutputs && hiddenOutputs.length == 0) {
setShowHiddenOutputs(false);
}
}, [hiddenOutputs]);
const memoizedNodeToolbarComponent = useMemo(() => {
return (
<NodeToolbar>

View file

@ -123,7 +123,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
title: UPLOAD_ERROR_ALERT,
list: [error],
});
}
},
);
}}
>
@ -193,7 +193,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
name="RefreshCcw"
className="header-menu-options"
/>
Reload Components
Refresh All
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -221,7 +221,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
)}
/>
{printByBuildStatus()}

View file

@ -244,7 +244,7 @@ export const nodeColors: { [char: string]: string } = {
outputs: "#AA2411",
data: "#198BF6",
prompts: "#4367BF",
models: "#6344BE",
models: "#ab11ab",
model_specs: "#6344BE",
chains: "#FE7500",
Document: "#7AAE42",
@ -271,10 +271,10 @@ export const nodeColors: { [char: string]: string } = {
Text: "#4367BF",
retrievers: "#e6b25a",
unknown: "#9CA3AF",
custom_components: "#ab11ab",
Data: "#31a3cc",
Data: "#31a3cc",
// custom_components: "#ab11ab",
Data: "#9CA3AF",
Message: "#4367BF",
BaseLanguageModel: "#ab11ab",
};
export const nodeNames: { [char: string]: string } = {