Merge branch 'zustand/io/migration' into state_theories
This commit is contained in:
commit
dc8ad28418
79 changed files with 1551 additions and 1566 deletions
|
|
@ -56,6 +56,13 @@ LANGFLOW_REMOVE_API_KEYS=
|
|||
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
|
||||
LANGFLOW_CACHE_TYPE=
|
||||
|
||||
|
||||
# Set AUTO_LOGIN to false if you want to disable auto login
|
||||
# and use the login form to login. LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD
|
||||
# must be set if AUTO_LOGIN is set to false
|
||||
# Values: true, false
|
||||
LANGFLOW_AUTO_LOGIN=
|
||||
|
||||
# Superuser username
|
||||
# Example: LANGFLOW_SUPERUSER=admin
|
||||
LANGFLOW_SUPERUSER=
|
||||
|
|
|
|||
|
|
@ -68,8 +68,6 @@ INVALID_CHARACTERS = {
|
|||
")",
|
||||
"[",
|
||||
"]",
|
||||
"{",
|
||||
"}",
|
||||
}
|
||||
|
||||
INVALID_NAMES = {
|
||||
|
|
@ -88,73 +86,110 @@ def validate_prompt(template: str):
|
|||
# Check if there are invalid characters in the input_variables
|
||||
input_variables = check_input_variables(input_variables)
|
||||
if any(var in INVALID_NAMES for var in input_variables):
|
||||
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ")
|
||||
raise ValueError(
|
||||
f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. "
|
||||
)
|
||||
|
||||
try:
|
||||
PromptTemplate(template=template, input_variables=input_variables)
|
||||
except Exception as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
raise ValueError(f"Invalid prompt: {exc}") from exc
|
||||
|
||||
return input_variables
|
||||
|
||||
|
||||
def check_input_variables(input_variables: list):
|
||||
def is_json_like(var):
|
||||
if var.startswith("{{") and var.endswith("}}"):
|
||||
# If it is a double brance variable
|
||||
# we don't want to validate any of its content
|
||||
return True
|
||||
# the above doesn't work on all cases because the json string can be multiline
|
||||
# or indented which can add \n or spaces at the start or end of the string
|
||||
# test_case_3 new_var == '\n{{\n "test": "hello",\n "text": "world"\n}}\n'
|
||||
# what we can do is to remove the \n and spaces from the start and end of the string
|
||||
# and then check if the string starts with {{ and ends with }}
|
||||
var = var.strip()
|
||||
var = var.replace("\n", "")
|
||||
var = var.replace(" ", "")
|
||||
# Now it should be a valid json string
|
||||
return var.startswith("{{") and var.endswith("}}")
|
||||
|
||||
|
||||
def fix_variable(var, invalid_chars, wrong_variables):
|
||||
if not var:
|
||||
return var, invalid_chars, wrong_variables
|
||||
new_var = var
|
||||
|
||||
# Handle variables starting with a number
|
||||
if var[0].isdigit():
|
||||
invalid_chars.append(var[0])
|
||||
new_var, invalid_chars, wrong_variables = fix_variable(
|
||||
var[1:], invalid_chars, wrong_variables
|
||||
)
|
||||
|
||||
# Temporarily replace {{ and }} to avoid treating them as invalid
|
||||
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ")
|
||||
|
||||
# Remove invalid characters
|
||||
for char in new_var:
|
||||
if char in INVALID_CHARACTERS:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
if var not in wrong_variables: # Avoid duplicating entries
|
||||
wrong_variables.append(var)
|
||||
|
||||
# Restore {{ and }}
|
||||
new_var = new_var.replace("ᴛᴇᴍᴘᴏᴘᴇɴ", "{{").replace("ᴛᴇᴍᴘᴄʟᴏsᴇ", "}}")
|
||||
|
||||
return new_var, invalid_chars, wrong_variables
|
||||
|
||||
|
||||
def check_variable(var, invalid_chars, wrong_variables, empty_variables):
|
||||
if any(char in invalid_chars for char in var):
|
||||
wrong_variables.append(var)
|
||||
elif var == "":
|
||||
empty_variables.append(var)
|
||||
return wrong_variables, empty_variables
|
||||
|
||||
|
||||
def check_for_errors(
|
||||
input_variables, fixed_variables, wrong_variables, empty_variables
|
||||
):
|
||||
if any(var for var in input_variables if var not in fixed_variables):
|
||||
error_message = (
|
||||
f"Error: Input variables contain invalid characters or formats. \n"
|
||||
f"Invalid variables: {', '.join(wrong_variables)}.\n"
|
||||
f"Empty variables: {', '.join(empty_variables)}. \n"
|
||||
f"Fixed variables: {', '.join(fixed_variables)}."
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
def check_input_variables(input_variables):
|
||||
invalid_chars = []
|
||||
fixed_variables = []
|
||||
wrong_variables = []
|
||||
empty_variables = []
|
||||
for variable in input_variables:
|
||||
new_var = variable
|
||||
variables_to_check = []
|
||||
|
||||
# if variable is empty, then we should add that to the wrong variables
|
||||
if not variable:
|
||||
empty_variables.append(variable)
|
||||
for var in input_variables:
|
||||
# First, let's check if the variable is a JSON string
|
||||
# because if it is, it won't be considered a variable
|
||||
# and we don't need to validate it
|
||||
if is_json_like(var):
|
||||
continue
|
||||
|
||||
# if variable starts with a number we should add that to the invalid chars
|
||||
# and wrong variables
|
||||
if variable[0].isdigit():
|
||||
invalid_chars.append(variable[0])
|
||||
new_var = new_var.replace(variable[0], "")
|
||||
wrong_variables.append(variable)
|
||||
else:
|
||||
for char in INVALID_CHARACTERS:
|
||||
if char in variable:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
wrong_variables.append(variable)
|
||||
fixed_variables.append(new_var)
|
||||
# If any of the input_variables is not in the fixed_variables, then it means that
|
||||
# there are invalid characters in the input_variables
|
||||
|
||||
if any(var not in fixed_variables for var in input_variables):
|
||||
error_message = build_error_message(
|
||||
input_variables,
|
||||
invalid_chars,
|
||||
wrong_variables,
|
||||
fixed_variables,
|
||||
empty_variables,
|
||||
new_var, wrong_variables, empty_variables = fix_variable(
|
||||
var, invalid_chars, wrong_variables
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
return input_variables
|
||||
wrong_variables, empty_variables = check_variable(
|
||||
var, INVALID_CHARACTERS, wrong_variables, empty_variables
|
||||
)
|
||||
fixed_variables.append(new_var)
|
||||
variables_to_check.append(var)
|
||||
|
||||
check_for_errors(
|
||||
variables_to_check, fixed_variables, wrong_variables, empty_variables
|
||||
)
|
||||
|
||||
def build_error_message(input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables):
|
||||
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
|
||||
error_string = f"Invalid input variables: {input_variables_str}. "
|
||||
|
||||
if wrong_variables and invalid_chars:
|
||||
# fix the wrong variables replacing invalid chars and find them in the fixed variables
|
||||
error_string_vars = "You can fix them by replacing the invalid characters: "
|
||||
wvars = wrong_variables.copy()
|
||||
for i, wrong_var in enumerate(wvars):
|
||||
for char in invalid_chars:
|
||||
wrong_var = wrong_var.replace(char, "")
|
||||
if wrong_var in fixed_variables:
|
||||
error_string_vars += f"'{wrong_variables[i]}' -> '{wrong_var}'"
|
||||
error_string += error_string_vars
|
||||
elif empty_variables:
|
||||
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
|
||||
elif len(set(fixed_variables)) != len(fixed_variables):
|
||||
error_string += "There are duplicate variables."
|
||||
return error_string
|
||||
return fixed_variables
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from langchain_core.documents import Document
|
||||
from typing import List
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
from langflow.utils.constants import LOADERS_INFO
|
||||
|
||||
|
||||
|
|
@ -10,7 +11,9 @@ class FileLoaderComponent(CustomComponent):
|
|||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
loader_options = ["Automatic"] + [loader_info["name"] for loader_info in LOADERS_INFO]
|
||||
loader_options = ["Automatic"] + [
|
||||
loader_info["name"] for loader_info in LOADERS_INFO
|
||||
]
|
||||
|
||||
file_types = []
|
||||
suffixes = []
|
||||
|
|
@ -74,7 +77,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(self, file_path: str, loader: str) -> Document:
|
||||
def build(self, file_path: str, loader: str) -> List[Record]:
|
||||
file_type = file_path.split(".")[-1]
|
||||
|
||||
# Map the loader to the correct loader class
|
||||
|
|
@ -102,7 +105,9 @@ class FileLoaderComponent(CustomComponent):
|
|||
if isinstance(selected_loader_info, dict):
|
||||
loader_import: str = selected_loader_info["import"]
|
||||
else:
|
||||
raise ValueError(f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}")
|
||||
raise ValueError(
|
||||
f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}"
|
||||
)
|
||||
module_name, class_name = loader_import.rsplit(".", 1)
|
||||
|
||||
try:
|
||||
|
|
@ -110,7 +115,10 @@ class FileLoaderComponent(CustomComponent):
|
|||
loader_module = __import__(module_name, fromlist=[class_name])
|
||||
loader_instance = getattr(loader_module, class_name)
|
||||
except ImportError as e:
|
||||
raise ValueError(f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}") from e
|
||||
raise ValueError(
|
||||
f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}"
|
||||
) from e
|
||||
|
||||
result = loader_instance(file_path=file_path)
|
||||
return result.load()
|
||||
docs = result.load()
|
||||
return self.to_records(docs)
|
||||
|
|
|
|||
|
|
@ -70,11 +70,17 @@ class GatherRecordsComponent(CustomComponent):
|
|||
|
||||
glob = "**/*" if recursive else "*"
|
||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
||||
file_paths = [
|
||||
Text(p)
|
||||
for p in paths
|
||||
if p.is_file() and match_types(p) and is_not_hidden(p)
|
||||
]
|
||||
|
||||
return file_paths
|
||||
|
||||
def parse_file_to_record(self, file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
def parse_file_to_record(
|
||||
self, file_path: str, silent_errors: bool
|
||||
) -> Optional[Record]:
|
||||
# Use the partition function to load the file
|
||||
from unstructured.partition.auto import partition # type: ignore
|
||||
|
||||
|
|
@ -100,9 +106,14 @@ class GatherRecordsComponent(CustomComponent):
|
|||
use_multithreading: bool,
|
||||
) -> List[Optional[Record]]:
|
||||
if use_multithreading:
|
||||
records = self.parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
records = self.parallel_load_records(
|
||||
file_paths, silent_errors, max_concurrency
|
||||
)
|
||||
else:
|
||||
records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
records = [
|
||||
self.parse_file_to_record(file_path, silent_errors)
|
||||
for file_path in file_paths
|
||||
]
|
||||
records = list(filter(None, records))
|
||||
return records
|
||||
|
||||
|
|
@ -131,13 +142,20 @@ class GatherRecordsComponent(CustomComponent):
|
|||
if types is None:
|
||||
types = []
|
||||
resolved_path = self.resolve_path(path)
|
||||
file_paths = self.retrieve_file_paths(resolved_path, types, load_hidden, recursive, depth)
|
||||
file_paths = self.retrieve_file_paths(
|
||||
resolved_path, types, load_hidden, recursive, depth
|
||||
)
|
||||
loaded_records = []
|
||||
|
||||
if use_multithreading:
|
||||
loaded_records = self.parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
loaded_records = self.parallel_load_records(
|
||||
file_paths, silent_errors, max_concurrency
|
||||
)
|
||||
else:
|
||||
loaded_records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
loaded_records = [
|
||||
self.parse_file_to_record(file_path, silent_errors)
|
||||
for file_path in file_paths
|
||||
]
|
||||
loaded_records = list(filter(None, loaded_records))
|
||||
self.status = loaded_records
|
||||
return loaded_records
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ class ChatOutput(ChatComponent):
|
|||
|
||||
def build(
|
||||
self,
|
||||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
sender: Optional[str] = "Machine",
|
||||
sender_name: Optional[str] = "AI",
|
||||
input_value: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
return_record: Optional[bool] = False,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,12 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.components.io.base.text import TextComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class TextInput(CustomComponent):
|
||||
class TextInput(TextComponent):
|
||||
display_name = "Text Input"
|
||||
description = "Used to pass text input to the next component."
|
||||
|
||||
field_config = {
|
||||
"input_value": {"display_name": "Value", "multiline": True},
|
||||
}
|
||||
|
||||
def build(self, input_value: Optional[str] = "") -> Text:
|
||||
self.status = input_value
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
return input_value
|
||||
return super().build(input_value=input_value)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.components.io.base.text import TextComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class TextOutput(CustomComponent):
|
||||
class TextOutput(TextComponent):
|
||||
display_name = "Text Output"
|
||||
description = "Used to pass text output to the next component."
|
||||
|
||||
field_config = {
|
||||
"value": {"display_name": "Value"},
|
||||
"input_value": {"display_name": "Value"},
|
||||
}
|
||||
|
||||
def build(self, value: Optional[str] = "") -> Text:
|
||||
self.status = value
|
||||
if not value:
|
||||
value = ""
|
||||
return value
|
||||
def build(self, input_value: Optional[Text] = "") -> Text:
|
||||
return super().build(input_value=input_value)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ class ChatComponent(CustomComponent):
|
|||
return []
|
||||
|
||||
if not session_id or not sender or not sender_name:
|
||||
raise ValueError("All of session_id, sender, and sender_name must be provided.")
|
||||
raise ValueError(
|
||||
"All of session_id, sender, and sender_name must be provided."
|
||||
)
|
||||
if isinstance(message, Record):
|
||||
record = message
|
||||
record.data.update(
|
||||
|
|
|
|||
19
src/backend/langflow/components/io/base/text.py
Normal file
19
src/backend/langflow/components/io/base/text.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class TextComponent(CustomComponent):
|
||||
display_name = "Text Component"
|
||||
description = "Used to pass text to the next component."
|
||||
|
||||
field_config = {
|
||||
"input_value": {"display_name": "Value", "multiline": True},
|
||||
}
|
||||
|
||||
def build(self, input_value: Optional[str] = "") -> Text:
|
||||
self.status = input_value
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
return input_value
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.helpers.record import records_to_text
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
|
|
@ -27,7 +28,6 @@ class RecordsAsTextComponent(CustomComponent):
|
|||
if isinstance(records, Record):
|
||||
records = [records]
|
||||
|
||||
formated_records = [template.format(text=record.text, data=record.data, **record.data) for record in records]
|
||||
result_string = "\n".join(formated_records)
|
||||
result_string = records_to_text(template, records)
|
||||
self.status = result_string
|
||||
return result_string
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ class ChromaSearchComponent(LCVectorStoreComponent):
|
|||
|
||||
if chroma_server_host is not None:
|
||||
chroma_settings = chromadb.config.Settings(
|
||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None,
|
||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
|
||||
or None,
|
||||
chroma_server_host=chroma_server_host,
|
||||
chroma_server_port=chroma_server_port or None,
|
||||
chroma_server_grpc_port=chroma_server_grpc_port or None,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ class FAISSSearchComponent(LCVectorStoreComponent):
|
|||
if not folder_path:
|
||||
raise ValueError("Folder path is required to save the FAISS index.")
|
||||
path = self.resolve_path(folder_path)
|
||||
vector_store = FAISS.load_local(folder_path=Text(path), embeddings=embedding, index_name=index_name)
|
||||
vector_store = FAISS.load_local(
|
||||
folder_path=Text(path), embeddings=embedding, index_name=index_name
|
||||
)
|
||||
if not vector_store:
|
||||
raise ValueError("Failed to load the FAISS index.")
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ class SupabaseSearchComponent(LCVectorStoreComponent):
|
|||
supabase_url: str = "",
|
||||
table_name: str = "",
|
||||
) -> List[Record]:
|
||||
supabase: Client = create_client(supabase_url, supabase_key=supabase_service_key)
|
||||
supabase: Client = create_client(
|
||||
supabase_url, supabase_key=supabase_service_key
|
||||
)
|
||||
vector_store = SupabaseVectorStore(
|
||||
client=supabase,
|
||||
embedding=embedding,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ from langflow.schema import Record
|
|||
class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent):
|
||||
display_name: str = "Vectara Search"
|
||||
description: str = "Search a Vectara Vector Store for similar documents."
|
||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/vectara"
|
||||
documentation = (
|
||||
"https://python.langchain.com/docs/integrations/vectorstores/vectara"
|
||||
)
|
||||
beta = True
|
||||
icon = "Vectara"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ from langflow.schema import Record
|
|||
class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent):
|
||||
display_name: str = "Weaviate Search"
|
||||
description: str = "Search a Weaviate Vector Store for similar documents."
|
||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/weaviate"
|
||||
documentation = (
|
||||
"https://python.langchain.com/docs/integrations/vectorstores/weaviate"
|
||||
)
|
||||
beta = True
|
||||
icon = "Weaviate"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from langchain_core.vectorstores import VectorStore
|
|||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.schema import Record, docs_to_records
|
||||
from langflow.helpers.record import docs_to_records
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class LCVectorStoreComponent(CustomComponent):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ class PGVectorSearchComponent(PGVectorComponent, LCVectorStoreComponent):
|
|||
|
||||
display_name: str = "PGVector Search"
|
||||
description: str = "Search a PGVector Store for similar documents."
|
||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/pgvector"
|
||||
documentation = (
|
||||
"https://python.langchain.com/docs/integrations/vectorstores/pgvector"
|
||||
)
|
||||
|
||||
def build_config(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
from typing import TYPE_CHECKING, Any, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langflow.graph.edge.utils import build_clean_params
|
||||
from langflow.graph.schema import INPUT_FIELD_NAME
|
||||
from langflow.services.deps import get_monitor_service
|
||||
from langflow.services.monitor.utils import log_message
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
|
||||
|
||||
class SourceHandle(BaseModel):
|
||||
baseClasses: List[str] = Field(..., description="List of base classes for the source handle.")
|
||||
baseClasses: List[str] = Field(
|
||||
..., description="List of base classes for the source handle."
|
||||
)
|
||||
dataType: str = Field(..., description="Data type for the source handle.")
|
||||
id: str = Field(..., description="Unique identifier for the source handle.")
|
||||
|
||||
|
|
@ -21,7 +22,9 @@ class SourceHandle(BaseModel):
|
|||
class TargetHandle(BaseModel):
|
||||
fieldName: str = Field(..., description="Field name for the target handle.")
|
||||
id: str = Field(..., description="Unique identifier for the target handle.")
|
||||
inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.")
|
||||
inputTypes: Optional[List[str]] = Field(
|
||||
None, description="List of input types for the target handle."
|
||||
)
|
||||
type: str = Field(..., description="Type of the target handle.")
|
||||
|
||||
|
||||
|
|
@ -50,16 +53,24 @@ class Edge:
|
|||
|
||||
def validate_handles(self, source, target) -> None:
|
||||
if self.target_handle.inputTypes is None:
|
||||
self.valid_handles = self.target_handle.type in self.source_handle.baseClasses
|
||||
self.valid_handles = (
|
||||
self.target_handle.type in self.source_handle.baseClasses
|
||||
)
|
||||
else:
|
||||
self.valid_handles = (
|
||||
any(baseClass in self.target_handle.inputTypes for baseClass in self.source_handle.baseClasses)
|
||||
any(
|
||||
baseClass in self.target_handle.inputTypes
|
||||
for baseClass in self.source_handle.baseClasses
|
||||
)
|
||||
or self.target_handle.type in self.source_handle.baseClasses
|
||||
)
|
||||
if not self.valid_handles:
|
||||
logger.debug(self.source_handle)
|
||||
logger.debug(self.target_handle)
|
||||
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has invalid handles")
|
||||
raise ValueError(
|
||||
f"Edge between {source.vertex_type} and {target.vertex_type} "
|
||||
f"has invalid handles"
|
||||
)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.source_id = state["source_id"]
|
||||
|
|
@ -76,7 +87,11 @@ class Edge:
|
|||
# Both lists contain strings and sometimes a string contains the value we are
|
||||
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
|
||||
# so we need to check if any of the strings in source_types is in target_reqs
|
||||
self.valid = any(output in target_req for output in self.source_types for target_req in self.target_reqs)
|
||||
self.valid = any(
|
||||
output in target_req
|
||||
for output in self.source_types
|
||||
for target_req in self.target_reqs
|
||||
)
|
||||
# Get what type of input the target node is expecting
|
||||
|
||||
self.matched_type = next(
|
||||
|
|
@ -87,7 +102,10 @@ class Edge:
|
|||
if no_matched_type:
|
||||
logger.debug(self.source_types)
|
||||
logger.debug(self.target_reqs)
|
||||
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has no matched type")
|
||||
raise ValueError(
|
||||
f"Edge between {source.vertex_type} and {target.vertex_type} "
|
||||
f"has no matched type"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
|
|
@ -98,8 +116,12 @@ class Edge:
|
|||
def __hash__(self) -> int:
|
||||
return hash(self.__repr__())
|
||||
|
||||
def __eq__(self, __value: object) -> bool:
|
||||
return self.__repr__() == __value.__repr__() if isinstance(__value, Edge) else False
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
# Create a better way to compare edges
|
||||
return (
|
||||
self._source_handle == __o._source_handle
|
||||
and self._target_handle == __o._target_handle
|
||||
)
|
||||
|
||||
|
||||
class ContractEdge(Edge):
|
||||
|
|
@ -156,7 +178,9 @@ class ContractEdge(Edge):
|
|||
return f"{self.source_id} -[{self.target_param}]-> {self.target_id}"
|
||||
|
||||
|
||||
def log_transaction(edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None):
|
||||
def log_transaction(
|
||||
edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None
|
||||
):
|
||||
try:
|
||||
monitor_service = get_monitor_service()
|
||||
clean_params = build_clean_params(target)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ from collections import defaultdict, deque
|
|||
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union
|
||||
|
||||
from langchain.chains.base import Chain
|
||||
from loguru import logger
|
||||
|
||||
from langflow.graph.edge.base import ContractEdge
|
||||
from langflow.graph.graph.constants import lazy_load_vertex_dict
|
||||
from langflow.graph.graph.state_manager import GraphStateManager
|
||||
|
|
@ -22,6 +20,7 @@ from langflow.graph.vertex.types import (
|
|||
from langflow.interface.tools.constants import FILE_TOOLS
|
||||
from langflow.schema import Record
|
||||
from langflow.utils import payload
|
||||
from loguru import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.schema import ResultData
|
||||
|
|
@ -278,7 +277,7 @@ class Graph:
|
|||
self.edges = new_edges
|
||||
|
||||
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
||||
data_is_equivalent = vertex.__repr__() == other_vertex.__repr__()
|
||||
data_is_equivalent = vertex == other_vertex
|
||||
if not data_is_equivalent:
|
||||
return False
|
||||
return self.vertex_edges_are_identical(vertex, other_vertex)
|
||||
|
|
@ -304,40 +303,52 @@ class Graph:
|
|||
# Find vertices that are in self but not in other (removed vertices)
|
||||
removed_vertex_ids = existing_vertex_ids - other_vertex_ids
|
||||
|
||||
# Update existing vertices that have changed
|
||||
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
|
||||
self_vertex = self.get_vertex(vertex_id)
|
||||
other_vertex = other.get_vertex(vertex_id)
|
||||
if not self.vertex_data_is_identical(self_vertex, other_vertex):
|
||||
self_vertex._data = other_vertex._data
|
||||
self_vertex._parse_data()
|
||||
# Now we update the edges of the vertex
|
||||
self.update_edges_from_vertex(self_vertex, other_vertex)
|
||||
self_vertex.params = {}
|
||||
self_vertex._build_params()
|
||||
self_vertex.graph = self
|
||||
# If the vertex is pinned, we don't want
|
||||
# to reset the results nor the _built attribute
|
||||
if not self_vertex.pinned:
|
||||
self_vertex._built = False
|
||||
self_vertex.result = None
|
||||
self_vertex.artifacts = {}
|
||||
self_vertex.set_top_level(self.top_level_vertices)
|
||||
self.reset_all_edges_of_vertex(self_vertex)
|
||||
|
||||
# Remove vertices
|
||||
# Remove vertices that are not in the other graph
|
||||
for vertex_id in removed_vertex_ids:
|
||||
self.remove_vertex(vertex_id)
|
||||
|
||||
# Add new vertices
|
||||
for vertex_id in new_vertex_ids:
|
||||
new_vertex = other.get_vertex(vertex_id)
|
||||
new_vertex.graph = self
|
||||
self._add_vertex(new_vertex)
|
||||
|
||||
# Update existing vertices that have changed
|
||||
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
|
||||
self_vertex = self.get_vertex(vertex_id)
|
||||
other_vertex = other.get_vertex(vertex_id)
|
||||
# If the vertices are not identical, update the vertex
|
||||
if not self.vertex_data_is_identical(self_vertex, other_vertex):
|
||||
self.update_vertex_from_another(self_vertex, other_vertex)
|
||||
|
||||
self.build_graph_maps()
|
||||
self.increment_update_count()
|
||||
return self
|
||||
|
||||
def update_vertex_from_another(self, vertex: Vertex, other_vertex: Vertex) -> None:
|
||||
"""
|
||||
Updates a vertex from another vertex.
|
||||
|
||||
Args:
|
||||
vertex (Vertex): The vertex to be updated.
|
||||
other_vertex (Vertex): The vertex to update from.
|
||||
"""
|
||||
vertex._data = other_vertex._data
|
||||
vertex._parse_data()
|
||||
# Now we update the edges of the vertex
|
||||
self.update_edges_from_vertex(vertex, other_vertex)
|
||||
vertex.params = {}
|
||||
vertex._build_params()
|
||||
vertex.graph = self
|
||||
# If the vertex is pinned, we don't want
|
||||
# to reset the results nor the _built attribute
|
||||
if not vertex.pinned:
|
||||
vertex._built = False
|
||||
vertex.result = None
|
||||
vertex.artifacts = {}
|
||||
vertex.set_top_level(self.top_level_vertices)
|
||||
self.reset_all_edges_of_vertex(vertex)
|
||||
|
||||
def reset_all_edges_of_vertex(self, vertex: Vertex) -> None:
|
||||
"""Resets all the edges of a vertex."""
|
||||
for edge in vertex.edges:
|
||||
|
|
@ -666,18 +677,42 @@ class Graph:
|
|||
"""Cuts the graph up to a given vertex and sorts the resulting subgraph."""
|
||||
# Initial setup
|
||||
visited = set() # To keep track of visited vertices
|
||||
excluded = set() # To keep track of vertices that should be excluded
|
||||
stack = [vertex_id] # Use a list as a stack for DFS
|
||||
|
||||
def get_successors(vertex, recursive=True):
|
||||
# Recursively get the successors of the current vertex
|
||||
successors = vertex.successors
|
||||
if not successors:
|
||||
return []
|
||||
successors_result = []
|
||||
for successor in successors:
|
||||
# Just return a list of successors
|
||||
if recursive:
|
||||
next_successors = get_successors(successor)
|
||||
successors_result.extend(next_successors)
|
||||
successors_result.append(successor)
|
||||
return successors_result
|
||||
|
||||
# DFS to collect all vertices that can reach the specified vertex
|
||||
while stack:
|
||||
current_id = stack.pop()
|
||||
if current_id not in visited:
|
||||
if current_id not in visited and current_id not in excluded:
|
||||
visited.add(current_id)
|
||||
current_vertex = self.get_vertex(current_id)
|
||||
# Assuming get_predecessors is a method that returns all vertices with edges to current_vertex
|
||||
for predecessor in current_vertex.predecessors:
|
||||
stack.append(predecessor.id)
|
||||
|
||||
if current_id == vertex_id:
|
||||
# We should add to visited all the vertices that are successors of the current vertex
|
||||
# and their successors and so on
|
||||
for successor in current_vertex.successors:
|
||||
excluded.add(successor.id)
|
||||
all_successors = get_successors(successor)
|
||||
for successor in all_successors:
|
||||
excluded.add(successor.id)
|
||||
|
||||
# Filter the original graph's vertices and edges to keep only those in `visited`
|
||||
vertices_to_keep = [self.get_vertex(vid) for vid in visited]
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue