Merge branch 'zustand/io/migration' of github.com:logspace-ai/langflow into zustand/io/migration

This commit is contained in:
igorrCarvalho 2024-03-07 18:00:10 -03:00
commit e120bfe869
44 changed files with 813 additions and 314 deletions

View file

@ -83,7 +83,8 @@ The CustomComponent class serves as the foundation for creating custom component
| _`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]`_. |
| _`title_case: bool`_ | Formats the name of the field when _`display_name`_ is not defined. Set it to False to keep the name as you set it in the _`build`_ method. |
| _`refresh: bool`_ | If set to True a button will appear to the right of the field, and when clicked, it will call the _`update_build_config`_ method which takes in the _`build_config`_, the name of the field (_`field_name`_) and the latest value of the field (_`field_value`_). This is useful when you want to update the _`build_config`_ based on the value of the field. |
| _`refresh_button: bool`_ | If set to True a button will appear to the right of the field, and when clicked, it will call the _`update_build_config`_ method which takes in the _`build_config`_, the name of the field (_`field_name`_) and the latest value of the field (_`field_value`_). This is useful when you want to update the _`build_config`_ based on the value of the field. |
| _`real_time_refresh: bool`_ | If set to True, the _`update_build_config`_ method will be called every time the field value changes. |
<Admonition type="info" label="Tip">

View file

@ -1,9 +1,7 @@
from typing import Optional
from langchain.prompts import PromptTemplate
from pydantic import BaseModel, field_validator, model_serializer
from langflow.interface.utils import extract_input_variables_from_prompt
from langflow.template.frontend_node.base import FrontendNode
@ -80,22 +78,6 @@ INVALID_NAMES = {
}
def validate_prompt(template: str):
input_variables = extract_input_variables_from_prompt(template)
# 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)}. ")
try:
PromptTemplate(template=template, input_variables=input_variables)
except Exception as exc:
raise ValueError(f"Invalid prompt: {exc}") from exc
return input_variables
def is_json_like(var):
if var.startswith("{{") and var.endswith("}}"):
# If it is a double brance variable
@ -121,7 +103,9 @@ def fix_variable(var, invalid_chars, wrong_variables):
# 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)
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ᴇ")
@ -148,7 +132,9 @@ def check_variable(var, invalid_chars, wrong_variables, empty_variables):
return wrong_variables, empty_variables
def check_for_errors(input_variables, fixed_variables, 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"
@ -173,11 +159,17 @@ def check_input_variables(input_variables):
if is_json_like(var):
continue
new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables)
wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables)
new_var, wrong_variables, empty_variables = fix_variable(
var, invalid_chars, wrong_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)
check_for_errors(
variables_to_check, fixed_variables, wrong_variables, empty_variables
)
return fixed_variables

View file

@ -93,7 +93,7 @@ async def build_vertex(
current_user=Depends(get_current_active_user),
):
"""Build a vertex instead of the entire graph."""
{"inputs": {"input_value": "some value"}}
start_time = time.perf_counter()
next_vertices_ids = []
try:
@ -101,8 +101,12 @@ async def build_vertex(
cache = chat_service.get_cache(flow_id)
if not cache:
# If there's no cache
logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}")
graph = build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
logger.warning(
f"No cache found for {flow_id}. Building graph starting at {vertex_id}"
)
graph = build_and_cache_graph(
flow_id=flow_id, session=next(get_session()), chat_service=chat_service
)
else:
graph = cache.get("result")
result_data_response = ResultDataResponse(results={})
@ -122,7 +126,9 @@ async def build_vertex(
else:
raise ValueError(f"No result found for vertex {vertex_id}")
next_vertices_ids = vertex.successors_ids
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
next_vertices_ids = [
v for v in next_vertices_ids if graph.should_run_vertex(v)
]
result_data_response = ResultDataResponse(**result_dict.model_dump())
@ -205,7 +211,9 @@ async def build_vertex_stream(
else:
graph = cache.get("result")
else:
session_data = await session_service.load_session(session_id, flow_id=flow_id)
session_data = await session_service.load_session(
session_id, flow_id=flow_id
)
graph, artifacts = session_data if session_data else (None, None)
if not graph:
raise ValueError(f"No graph found for {flow_id}.")

View file

@ -6,9 +6,14 @@ from langflow.api.v1.base import (
CodeValidationResponse,
PromptValidationResponse,
ValidatePromptRequest,
)
from langflow.base.prompts.utils import (
add_new_variables_to_template,
get_old_custom_fields,
remove_old_variables_from_template,
update_input_variables_field,
validate_prompt,
)
from langflow.template.field.prompt import DefaultPromptField
from langflow.utils.validate import validate_code
# build router
@ -37,13 +42,28 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
input_variables=input_variables,
frontend_node=None,
)
old_custom_fields = get_old_custom_fields(prompt_request)
old_custom_fields = get_old_custom_fields(
prompt_request.custom_fields, prompt_request.name
)
add_new_variables_to_template(input_variables, prompt_request)
add_new_variables_to_template(
input_variables,
prompt_request.custom_fields,
prompt_request.frontend_node.template,
prompt_request.name,
)
remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request)
remove_old_variables_from_template(
old_custom_fields,
input_variables,
prompt_request.custom_fields,
prompt_request.frontend_node.template,
prompt_request.name,
)
update_input_variables_field(input_variables, prompt_request)
update_input_variables_field(
input_variables, prompt_request.frontend_node.template
)
return PromptValidationResponse(
input_variables=input_variables,
@ -52,61 +72,3 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e
def get_old_custom_fields(prompt_request):
try:
if len(prompt_request.frontend_node.custom_fields) == 1 and prompt_request.name == "":
# If there is only one custom field and the name is empty string
# then we are dealing with the first prompt request after the node was created
prompt_request.name = list(prompt_request.frontend_node.custom_fields.keys())[0]
old_custom_fields = prompt_request.frontend_node.custom_fields[prompt_request.name]
if old_custom_fields is None:
old_custom_fields = []
old_custom_fields = old_custom_fields.copy()
except KeyError:
old_custom_fields = []
prompt_request.frontend_node.custom_fields[prompt_request.name] = []
return old_custom_fields
def add_new_variables_to_template(input_variables, prompt_request):
for variable in input_variables:
try:
template_field = DefaultPromptField(name=variable, display_name=variable)
if variable in prompt_request.frontend_node.template:
# Set the new field with the old value
template_field.value = prompt_request.frontend_node.template[variable]["value"]
prompt_request.frontend_node.template[variable] = template_field.to_dict()
# Check if variable is not already in the list before appending
if variable not in prompt_request.frontend_node.custom_fields[prompt_request.name]:
prompt_request.frontend_node.custom_fields[prompt_request.name].append(variable)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
def remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request):
for variable in old_custom_fields:
if variable not in input_variables:
try:
# Remove the variable from custom_fields associated with the given name
if variable in prompt_request.frontend_node.custom_fields[prompt_request.name]:
prompt_request.frontend_node.custom_fields[prompt_request.name].remove(variable)
# Remove the variable from the template
prompt_request.frontend_node.template.pop(variable, None)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
def update_input_variables_field(input_variables, prompt_request):
if "input_variables" in prompt_request.frontend_node.template:
prompt_request.frontend_node.template["input_variables"]["value"] = input_variables

View file

@ -1,6 +1,12 @@
from fastapi import HTTPException
from langchain.prompts import PromptTemplate
from langchain_core.documents import Document
from loguru import logger
from langflow.api.v1.base import INVALID_NAMES, check_input_variables
from langflow.interface.utils import extract_input_variables_from_prompt
from langflow.schema import Record
from langflow.template.field.prompt import DefaultPromptField
def dict_values_to_string(d: dict) -> dict:
@ -53,3 +59,83 @@ def document_to_string(document: Document) -> str:
str: The document as a string.
"""
return document.page_content
def validate_prompt(prompt_template: str, silent_errors: bool = False) -> list[str]:
input_variables = extract_input_variables_from_prompt(prompt_template)
# 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)}. "
)
try:
PromptTemplate(template=prompt_template, input_variables=input_variables)
except Exception as exc:
logger.error(f"Invalid prompt: {exc}")
if not silent_errors:
raise ValueError(f"Invalid prompt: {exc}") from exc
return input_variables
def get_old_custom_fields(custom_fields, name):
try:
if len(custom_fields) == 1 and name == "":
# If there is only one custom field and the name is empty string
# then we are dealing with the first prompt request after the node was created
name = list(custom_fields.keys())[0]
old_custom_fields = custom_fields[name]
if not old_custom_fields:
old_custom_fields = []
old_custom_fields = old_custom_fields.copy()
except KeyError:
old_custom_fields = []
custom_fields[name] = []
return old_custom_fields
def add_new_variables_to_template(input_variables, custom_fields, template, name):
for variable in input_variables:
try:
template_field = DefaultPromptField(name=variable, display_name=variable)
if variable in template:
# Set the new field with the old value
template_field.value = template[variable]["value"]
template[variable] = template_field.to_dict()
# Check if variable is not already in the list before appending
if variable not in custom_fields[name]:
custom_fields[name].append(variable)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
def remove_old_variables_from_template(
old_custom_fields, input_variables, custom_fields, template, name
):
for variable in old_custom_fields:
if variable not in input_variables:
try:
# Remove the variable from custom_fields associated with the given name
if variable in custom_fields[name]:
custom_fields[name].remove(variable)
# Remove the variable from the template
template.pop(variable, None)
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
def update_input_variables_field(input_variables, template):
if "input_variables" in template:
template["input_variables"]["value"] = input_variables

View file

@ -1,12 +1,11 @@
import asyncio
import json
from typing import List, Optional
import httpx
import json
from langflow import CustomComponent
from langflow.schema import Record
from langflow.services.database.models.base import orjson_dumps
class APIRequest(CustomComponent):
@ -52,35 +51,44 @@ class APIRequest(CustomComponent):
timeout: int = 5,
) -> Record:
method = method.upper()
if method not in ["GET", "POST", "PATCH", "PUT"]:
if method not in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
raise ValueError(f"Unsupported method: {method}")
data = body if body else None
data = json.dumps(data)
try:
response = await client.request(method, url, headers=headers, content=data, timeout=timeout)
response = await client.request(
method, url, headers=headers, content=data, timeout=timeout
)
try:
response_json = response.json()
result = orjson_dumps(response_json, indent_2=False)
result = response.json()
except Exception:
result = response.text
return Record(
text=result,
data={
"source": url,
"headers": headers,
"status_code": response.status_code,
"result": result,
},
)
except httpx.TimeoutException:
return Record(
text="Request Timed Out",
data={"source": url, "headers": headers, "status_code": 408},
data={
"source": url,
"headers": headers,
"status_code": 408,
"error": "Request timed out",
},
)
except Exception as exc:
return Record(
text=str(exc),
data={"source": url, "headers": headers, "status_code": 500},
data={
"source": url,
"headers": headers,
"status_code": 500,
"error": str(exc),
},
)
async def build(
@ -88,15 +96,23 @@ class APIRequest(CustomComponent):
method: str,
url: List[str],
headers: Optional[dict] = None,
body: Optional[dict] = None,
body: Optional[List[Record]] = None,
timeout: int = 5,
) -> List[Record]:
if headers is None:
headers = {}
urls = url if isinstance(url, list) else [url]
bodies = body if isinstance(body, list) else [body] if body else [None] * len(urls)
bodies = []
if body:
if isinstance(body, list):
bodies = [b.data for b in body]
else:
bodies = [body.data]
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
*[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)]
*[
self.make_request(client, method, u, headers, rec, timeout)
for u, rec in zip(urls, bodies)
]
)
return results

View file

@ -80,7 +80,7 @@ class TextToRecordComponent(CustomComponent):
"display_name": "Mode",
"options": ["Text", "Number"],
"info": "The mode to use for creating the record.",
"refresh": True,
"real_time_refresh": True,
"input_types": [],
},
}

View file

@ -9,7 +9,9 @@ class UUIDGeneratorComponent(CustomComponent):
display_name = "Unique ID Generator"
description = "Generates a unique ID."
def update_build_config(self, build_config: dict, field_name: Text, field_value: Any):
def update_build_config(
self, build_config: dict, field_name: Text, field_value: Any
):
if field_name == "unique_id":
build_config[field_name]["value"] = str(uuid.uuid4())
return build_config
@ -18,7 +20,7 @@ class UUIDGeneratorComponent(CustomComponent):
return {
"unique_id": {
"display_name": "Value",
"refresh": True,
"real_time_refresh": True,
}
}

View file

@ -1,47 +0,0 @@
from typing import Any
from langflow import CustomComponent
from langflow.schema import Record
from langflow.template.field.base import TemplateField
class RecordComponent(CustomComponent):
display_name = "Record Numbers"
description = "A component to create a record from key-value pairs."
field_order = ["n_keys"]
def update_build_config(self, build_config: dict, field_name: str, field_value: Any):
if field_value is None:
return
elif int(field_value) == 0:
keep = ["n_keys", "code"]
for key in build_config.copy():
if key in keep:
continue
del build_config[key]
build_config[field_name]["value"] = int(field_value)
# Add new fields depending on the field value
for i in range(int(field_value)):
field = TemplateField(
name=f"Key and Value {i}",
field_type="dict",
display_name="",
info="The key for the record.",
input_types=["Text"],
)
build_config[field.name] = field.to_dict()
def build_config(self):
return {
"n_keys": {
"display_name": "Number of Fields",
"refresh": True,
"info": "The number of keys to create in the record.",
},
}
def build(self, n_keys: int, **kwargs) -> Record:
data = {k: v for d in kwargs.values() for k, v in d.items()}
record = Record(data=data)
return record

View file

@ -9,6 +9,7 @@ class PromptComponent(CustomComponent):
display_name: str = "Prompt"
description: str = "A component for creating prompts using templates"
beta = True
icon = "terminal-square"
def build_config(self):
return {

View file

@ -7,7 +7,7 @@ from langflow.field_typing import Text
class AmazonBedrockComponent(LCModelComponent):
display_name: str = "Amazon Bedrock Model"
display_name: str = "Amazon Bedrock"
description: str = "Generate text using LLM model from Amazon Bedrock."
icon = "Amazon"

View file

@ -8,7 +8,7 @@ from langflow.field_typing import Text
class AnthropicLLM(LCModelComponent):
display_name: str = "AnthropicModel"
display_name: str = "Anthropic"
description: str = "Generate text using Anthropic Chat&Completion large language models."
icon = "Anthropic"

View file

@ -9,7 +9,7 @@ from langflow.field_typing import Text
class AzureChatOpenAIComponent(LCModelComponent):
display_name: str = "AzureOpenAI Model"
display_name: str = "AzureOpenAI"
description: str = "Generate text using LLM model from Azure OpenAI."
documentation: str = "https://python.langchain.com/docs/integrations/llms/azure_openai"
beta = False

View file

@ -8,7 +8,7 @@ from langflow.field_typing import Text
class QianfanChatEndpointComponent(LCModelComponent):
display_name: str = "QianfanChat Model"
display_name: str = "QianfanChat"
description: str = (
"Generate text using Baidu Qianfan chat models. Get more detail from "
"https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint."

View file

@ -7,7 +7,7 @@ from langflow.field_typing import Text
class CTransformersComponent(LCModelComponent):
display_name = "CTransformersModel"
display_name = "CTransformers"
description = "Generate text using CTransformers LLM models"
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/ctransformers"

View file

@ -5,7 +5,7 @@ from langflow.field_typing import Text
class CohereComponent(LCModelComponent):
display_name = "CohereModel"
display_name = "Cohere"
description = "Generate text using Cohere large language models."
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/cohere"

View file

@ -8,7 +8,7 @@ from langflow.field_typing import RangeSpec, Text
class GoogleGenerativeAIComponent(LCModelComponent):
display_name: str = "Google Generative AIModel"
display_name: str = "Google Generative AI"
description: str = "Generate text using Google Generative AI to generate text."
icon = "GoogleGenerativeAI"
icon = "Google"

View file

@ -8,7 +8,7 @@ from langflow.field_typing import Text
class HuggingFaceEndpointsComponent(LCModelComponent):
display_name: str = "Hugging Face Inference API models"
display_name: str = "Hugging Face Inference API"
description: str = "Generate text using LLM model from Hugging Face Inference API."
icon = "HuggingFace"

View file

@ -7,7 +7,7 @@ from langflow.field_typing import Text
class LlamaCppComponent(LCModelComponent):
display_name = "LlamaCppModel"
display_name = "LlamaCpp"
description = "Generate text using llama.cpp model."
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/llamacpp"

View file

@ -13,7 +13,7 @@ from langflow.field_typing import Text
class ChatOllamaComponent(LCModelComponent):
display_name = "ChatOllamaModel"
display_name = "ChatOllama"
description = "Generate text using Local LLM for chat with Ollama."
icon = "Ollama"

View file

@ -8,7 +8,7 @@ from langflow.field_typing import NestedDict, Text
class OpenAIModelComponent(LCModelComponent):
display_name = "OpenAI Model"
display_name = "OpenAI"
description = "Generates text using OpenAI's models."
icon = "OpenAI"

View file

@ -7,7 +7,7 @@ from langflow.field_typing import Text
class ChatVertexAIComponent(LCModelComponent):
display_name = "ChatVertexAIModel"
display_name = "ChatVertexAI"
description = "Generate text using Vertex AI Chat large language models API."
icon = "VertexAI"

View file

@ -76,7 +76,9 @@ class Graph:
"""Returns the state of the graph."""
return self.state_manager.get_state(name, run_id=self._run_id)
def update_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None:
def update_state(
self, name: str, record: Union[str, Record], caller: Optional[str] = None
) -> None:
"""Updates the state of the graph."""
if caller:
# If there is a caller which is a vertex_id, I want to activate
@ -108,7 +110,9 @@ class Graph:
def reset_activated_vertices(self):
self.activated_vertices = []
def append_state(self, name: str, record: Union[str, Record], caller: Optional[str] = None) -> None:
def append_state(
self, name: str, record: Union[str, Record], caller: Optional[str] = None
) -> None:
"""Appends the state of the graph."""
if caller:
self.activate_state_vertices(name, caller)
@ -156,7 +160,10 @@ class Graph:
"""Runs the graph with the given inputs."""
for vertex_id in self._is_input_vertices:
vertex = self.get_vertex(vertex_id)
if input_components and (vertex_id not in input_components or vertex.display_name not in input_components):
if input_components and (
vertex_id not in input_components
or vertex.display_name not in input_components
):
continue
if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found")
@ -179,9 +186,13 @@ class Graph:
if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found")
if not vertex.result and not stream and hasattr(vertex, "consume_async_generator"):
if (
not vertex.result
and not stream
and hasattr(vertex, "consume_async_generator")
):
await vertex.consume_async_generator()
if vertex.display_name in outputs or vertex.id in outputs:
if not outputs or (vertex.display_name in outputs or vertex.id in outputs):
vertex_outputs.append(vertex.result)
return vertex_outputs
@ -189,8 +200,8 @@ class Graph:
self,
inputs: Dict[str, Union[str, list[str]]],
outputs: list[str],
stream: bool,
session_id: str,
stream: Optional[bool] = False,
) -> List[Optional["ResultData"]]:
"""Runs the graph with the given inputs."""
@ -257,7 +268,9 @@ class Graph:
def build_parent_child_map(self):
parent_child_map = defaultdict(list)
for vertex in self.vertices:
parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)]
parent_child_map[vertex.id] = [
child.id for child in self.get_successors(vertex)
]
return parent_child_map
def increment_run_count(self):
@ -442,7 +455,11 @@ class Graph:
"""Updates the edges of a vertex."""
# Vertex has edges, so we need to update the edges
for edge in vertex.edges:
if edge not in self.edges and edge.source_id in self.vertex_map and edge.target_id in self.vertex_map:
if (
edge not in self.edges
and edge.source_id in self.vertex_map
and edge.target_id in self.vertex_map
):
self.edges.append(edge)
def _build_graph(self) -> None:
@ -467,7 +484,11 @@ class Graph:
return
self.vertices.remove(vertex)
self.vertex_map.pop(vertex_id)
self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id]
self.edges = [
edge
for edge in self.edges
if edge.source_id != vertex_id and edge.target_id != vertex_id
]
def _build_vertex_params(self) -> None:
"""Identifies and handles the LLM vertex within the graph."""
@ -488,7 +509,9 @@ class Graph:
return
for vertex in self.vertices:
if not self._validate_vertex(vertex):
raise ValueError(f"{vertex.display_name} is not connected to any other components")
raise ValueError(
f"{vertex.display_name} is not connected to any other components"
)
def _validate_vertex(self, vertex: Vertex) -> bool:
"""Validates a vertex."""
@ -550,7 +573,9 @@ class Graph:
name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}",
)
tasks.append(task)
vertex_task_run_count[vertex_id] = vertex_task_run_count.get(vertex_id, 0) + 1
vertex_task_run_count[vertex_id] = (
vertex_task_run_count.get(vertex_id, 0) + 1
)
logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks")
await self._execute_tasks(tasks)
logger.debug("Graph processing complete")
@ -592,7 +617,9 @@ class Graph:
def dfs(vertex):
if state[vertex] == 1:
# We have a cycle
raise ValueError("Graph contains a cycle, cannot perform topological sort")
raise ValueError(
"Graph contains a cycle, cannot perform topological sort"
)
if state[vertex] == 0:
state[vertex] = 1
for edge in vertex.edges:
@ -616,7 +643,10 @@ class Graph:
def get_predecessors(self, vertex):
"""Returns the predecessors of a vertex."""
return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
return [
self.get_vertex(source_id)
for source_id in self.predecessor_map.get(vertex.id, [])
]
def get_all_successors(self, vertex, recursive=True, flat=True):
# Recursively get the successors of the current vertex
@ -657,7 +687,10 @@ class Graph:
def get_successors(self, vertex):
"""Returns the successors of a vertex."""
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])]
return [
self.get_vertex(target_id)
for target_id in self.successor_map.get(vertex.id, [])
]
def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]:
"""Returns the neighbors of a vertex."""
@ -703,7 +736,9 @@ class Graph:
edges_added.add((source.id, target.id))
return edges
def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]:
def _get_vertex_class(
self, node_type: str, node_base_type: str, node_id: str
) -> Type[Vertex]:
"""Returns the node class based on the node type."""
# First we check for the node_base_type
node_name = node_id.split("-")[0]
@ -736,14 +771,18 @@ class Graph:
vertex_type: str = vertex_data["type"] # type: ignore
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore
VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"])
VertexClass = self._get_vertex_class(
vertex_type, vertex_base_type, vertex_data["id"]
)
vertex_instance = VertexClass(vertex, graph=self)
vertex_instance.set_top_level(self.top_level_vertices)
vertices.append(vertex_instance)
return vertices
def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]:
def get_children_by_vertex_type(
self, vertex: Vertex, vertex_type: str
) -> List[Vertex]:
"""Returns the children of a vertex based on the vertex type."""
children = []
vertex_types = [vertex.data["type"]]
@ -755,7 +794,9 @@ class Graph:
def __repr__(self):
vertex_ids = [vertex.id for vertex in self.vertices]
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
edges_repr = "\n".join(
[f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]
)
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]:
@ -823,7 +864,8 @@ class Graph:
vertex.id
for vertex in vertices
# if filter_graphs then only vertex.is_input will be considered
if self.in_degree_map[vertex.id] == 0 and (not filter_graphs or vertex.is_input)
if self.in_degree_map[vertex.id] == 0
and (not filter_graphs or vertex.is_input)
)
layers: List[List[str]] = []
visited = set(queue)
@ -897,7 +939,9 @@ class Graph:
return refined_layers
def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
def sort_chat_inputs_first(
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
chat_inputs_first = []
for layer in vertices_layers:
for vertex_id in layer:
@ -938,7 +982,9 @@ class Graph:
first_layer = vertices_layers[0]
# save the only the rest
self.vertices_layers = vertices_layers[1:]
self.vertices_to_run = {vertex_id for vertex_id in chain.from_iterable(vertices_layers)}
self.vertices_to_run = {
vertex_id for vertex_id in chain.from_iterable(vertices_layers)
}
# Return just the first layer
return first_layer
@ -949,11 +995,15 @@ class Graph:
self.vertices_to_run.remove(vertex_id)
return should_run
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
def sort_interface_components_first(
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
def contains_interface_component(vertex):
return any(component.value in vertex for component in InterfaceComponentTypes)
return any(
component.value in vertex for component in InterfaceComponentTypes
)
# Sort each inner list so that vertices containing ChatInput or ChatOutput come first
sorted_vertices = [
@ -965,16 +1015,22 @@ class Graph:
]
return sorted_vertices
def sort_by_avg_build_time(self, vertices_layers: List[List[str]]) -> List[List[str]]:
def sort_by_avg_build_time(
self, vertices_layers: List[List[str]]
) -> List[List[str]]:
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
def sort_layer_by_avg_build_time(vertices_ids: List[str]) -> List[str]:
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
if len(vertices_ids) == 1:
return vertices_ids
vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time)
vertices_ids.sort(
key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time
)
return vertices_ids
sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers]
sorted_vertices = [
sort_layer_by_avg_build_time(layer) for layer in vertices_layers
]
return sorted_vertices

View file

@ -18,6 +18,7 @@ from loguru import logger
from langflow.graph.schema import (
INPUT_COMPONENTS,
INPUT_FIELD_NAME,
OUTPUT_COMPONENTS,
InterfaceComponentTypes,
ResultData,
@ -709,7 +710,8 @@ class Vertex:
self._reset()
if self._is_chat_input() and inputs is not None:
self.update_raw_params(inputs)
inputs = {"input_value": inputs.get(INPUT_FIELD_NAME, "")}
self.update_raw_params(inputs, overwrite=True)
# Run steps
for step in self.steps:

View file

@ -6,8 +6,9 @@ from emoji import demojize, purely_emoji
from loguru import logger
from sqlmodel import select
from langflow.interface.types import get_all_components
from langflow.services.database.models.flow.model import Flow, FlowCreate
from langflow.services.deps import session_scope
from langflow.services.deps import get_settings_service, session_scope
STARTER_FOLDER_NAME = "Starter Projects"
@ -17,6 +18,23 @@ STARTER_FOLDER_NAME = "Starter Projects"
# can use them as a starting point for their own projects.
def update_projects_components_with_latest_component_versions(
project_data, all_types_dict
):
# project data has a nodes key, which is a list of nodes
# we want to run through each node and see if it exists in the all_types_dict
# if so, we go into the template key and also get the template from all_types_dict
# and update it all
for node in project_data.get("nodes", []):
node_data = node.get("data").get("node")
if node_data.get("display_name") in all_types_dict:
latest_node = all_types_dict.get(node_data.get("display_name"))
latest_template = latest_node.get("template")
node_data["template"]["code"] = latest_template["code"]
return project_data
def load_starter_projects():
starter_projects = []
folder = Path(__file__).parent / "starter_projects"
@ -115,6 +133,8 @@ def delete_start_projects(session):
def create_or_update_starter_projects():
components_paths = get_settings_service().settings.COMPONENTS_PATH
all_types_dict = get_all_components(components_paths, as_dict=True)
with session_scope() as session:
starter_projects = load_starter_projects()
delete_start_projects(session)
@ -128,6 +148,9 @@ def create_or_update_starter_projects():
project_icon,
project_icon_bg_color,
) = get_project_data(project)
project_data = update_projects_components_with_latest_component_versions(
project_data, all_types_dict
)
if project_name and project_data:
for existing_project in get_all_flows_similar_to_project(
session, project_name

View file

@ -9,7 +9,11 @@ from fastapi import HTTPException
from loguru import logger
from langflow.interface.custom.eval import eval_custom_component_code
from langflow.interface.custom.schema import CallableCodeDetails, ClassCodeDetails
from langflow.interface.custom.schema import (
CallableCodeDetails,
ClassCodeDetails,
MissingDefault,
)
class CodeSyntaxError(HTTPException):
@ -95,7 +99,9 @@ class CodeParser:
elif isinstance(node, ast.ImportFrom):
for alias in node.names:
if alias.asname:
self.data["imports"].append((node.module, f"{alias.name} as {alias.asname}"))
self.data["imports"].append(
(node.module, f"{alias.name} as {alias.asname}")
)
else:
self.data["imports"].append((node.module, alias.name))
@ -144,7 +150,9 @@ class CodeParser:
return_type = None
if node.returns:
return_type_str = ast.unparse(node.returns)
eval_env = self.construct_eval_env(return_type_str, tuple(self.data["imports"]))
eval_env = self.construct_eval_env(
return_type_str, tuple(self.data["imports"])
)
try:
return_type = eval(return_type_str, eval_env)
@ -185,15 +193,23 @@ class CodeParser:
num_args = len(node.args.args)
num_defaults = len(node.args.defaults)
num_missing_defaults = num_args - num_defaults
missing_defaults = [None] * num_missing_defaults
default_values = [ast.unparse(default).strip("'") if default else None for default in node.args.defaults]
missing_defaults = [MissingDefault()] * num_missing_defaults
default_values = [
ast.unparse(default).strip("'") if default else None
for default in node.args.defaults
]
# Now check all default values to see if there
# are any "None" values in the middle
default_values = [None if value == "None" else value for value in default_values]
default_values = [
None if value == "None" else value for value in default_values
]
defaults = missing_defaults + default_values
args = [self.parse_arg(arg, default) for arg, default in zip(node.args.args, defaults)]
args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.args, defaults)
]
return args
def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
@ -211,11 +227,17 @@ class CodeParser:
"""
Parses the keyword-only arguments of a function or method node.
"""
kw_defaults = [None] * (len(node.args.kwonlyargs) - len(node.args.kw_defaults)) + [
ast.unparse(default) if default else None for default in node.args.kw_defaults
kw_defaults = [None] * (
len(node.args.kwonlyargs) - len(node.args.kw_defaults)
) + [
ast.unparse(default) if default else None
for default in node.args.kw_defaults
]
args = [self.parse_arg(arg, default) for arg, default in zip(node.args.kwonlyargs, kw_defaults)]
args = [
self.parse_arg(arg, default)
for arg, default in zip(node.args.kwonlyargs, kw_defaults)
]
return args
def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
@ -319,7 +341,9 @@ class CodeParser:
Extracts global variables from the code.
"""
global_var = {
"targets": [t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets],
"targets": [
t.id if hasattr(t, "id") else ast.dump(t) for t in node.targets
],
"value": ast.unparse(node.value),
}
self.data["global_vars"].append(global_var)

View file

@ -24,6 +24,7 @@ from langflow.interface.custom.code_parser.utils import (
)
from langflow.interface.custom.custom_component.component import Component
from langflow.schema import Record
from langflow.schema.dotdict import dotdict
from langflow.services.database.models.flow import Flow
from langflow.services.database.utils import session_getter
from langflow.services.deps import (
@ -77,13 +78,17 @@ class CustomComponent(Component):
def update_state(self, name: str, value: Any):
try:
self.vertex.graph.update_state(name=name, record=value, caller=self.vertex.id)
self.vertex.graph.update_state(
name=name, record=value, caller=self.vertex.id
)
except Exception as e:
raise ValueError(f"Error updating state: {e}")
def append_state(self, name: str, value: Any):
try:
self.vertex.graph.append_state(name=name, record=value, caller=self.vertex.id)
self.vertex.graph.append_state(
name=name, record=value, caller=self.vertex.id
)
except Exception as e:
raise ValueError(f"Error appending state: {e}")
@ -134,7 +139,12 @@ class CustomComponent(Component):
def build_config(self):
return self.field_config
def update_build_config(self, build_config: dict, field_name: str, field_value: Any):
def update_build_config(
self,
build_config: dotdict,
field_name: str,
field_value: Any,
):
build_config[field_name] = field_value
return build_config
@ -142,7 +152,9 @@ class CustomComponent(Component):
def tree(self):
return self.get_code_tree(self.code or "")
def to_records(self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False) -> List[Record]:
def to_records(
self, data: Any, keys: Optional[List[str]] = None, silent_errors: bool = False
) -> List[Record]:
"""
Converts input data into a list of Record objects.
@ -191,7 +203,9 @@ class CustomComponent(Component):
return records
def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str:
def create_references_from_records(
self, records: List[Record], include_data: bool = False
) -> str:
"""
Create references from a list of records.
@ -230,14 +244,20 @@ class CustomComponent(Component):
if not self.code:
return {}
component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]]
component_classes = [
cls
for cls in self.tree["classes"]
if self.code_class_base_inheritance in cls["bases"]
]
if not component_classes:
return {}
# Assume the first Component class is the one we're interested in
component_class = component_classes[0]
build_methods = [
method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name
method
for method in component_class["methods"]
if method["name"] == self.function_entrypoint_name
]
return build_methods[0] if build_methods else {}
@ -294,7 +314,9 @@ class CustomComponent(Component):
# Retrieve and decrypt the credential by name for the current user
db_service = get_db_service()
with session_getter(db_service) as session:
return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session)
return credential_service.get_credential(
user_id=self._user_id or "", name=name, session=session
)
return get_credential
@ -304,7 +326,9 @@ class CustomComponent(Component):
credential_service = get_credential_service()
db_service = get_db_service()
with session_getter(db_service) as session:
return credential_service.list_credentials(user_id=self._user_id, session=session)
return credential_service.list_credentials(
user_id=self._user_id, session=session
)
def index(self, value: int = 0):
"""Returns a function that returns the value at the given index in the iterable."""
@ -343,7 +367,11 @@ class CustomComponent(Component):
if not self._flows_records:
self.list_flows()
if not flow_id and self._flows_records:
flow_ids = [flow.data["id"] for flow in self._flows_records if flow.data["name"] == flow_name]
flow_ids = [
flow.data["id"]
for flow in self._flows_records
if flow.data["name"] == flow_name
]
if not flow_ids:
raise ValueError(f"Flow {flow_name} not found")
elif len(flow_ids) > 1:
@ -365,7 +393,9 @@ class CustomComponent(Component):
db_service = get_db_service()
with get_session(db_service) as session:
flows = session.exec(
select(Flow).where(Flow.user_id == self._user_id).where(Flow.is_component == False) # noqa
select(Flow)
.where(Flow.user_id == self._user_id)
.where(Flow.is_component == False) # noqa
).all()
flows_records = [flow.to_record() for flow in flows]

View file

@ -27,3 +27,12 @@ class CallableCodeDetails(BaseModel):
body: list
return_type: Optional[Any] = None
has_return: bool = False
class MissingDefault:
"""
A class to represent a missing default value.
"""
def __repr__(self):
return "MISSING"

View file

@ -20,6 +20,8 @@ from langflow.interface.custom.directory_reader.utils import (
merge_nested_dicts_with_renaming,
)
from langflow.interface.custom.eval import eval_custom_component_code
from langflow.interface.custom.schema import MissingDefault
from langflow.schema import dotdict
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.custom_components import (
CustomComponentFrontendNode,
@ -110,7 +112,7 @@ def extract_type_from_optional(field_type):
str: The extracted type, or an empty string if no type was found.
"""
match = re.search(r"\[(.*?)\]$", field_type)
return match[1] if match else None
return match[1] if match else field_type
def get_field_properties(extra_field):
@ -118,7 +120,13 @@ def get_field_properties(extra_field):
field_name = extra_field["name"]
field_type = extra_field.get("type", "str")
field_value = extra_field.get("default", "")
field_required = "optional" not in field_type.lower()
# a required field is a field that does not contain
# optional in field_type
# and a field that does not have a default value
field_required = "optional" not in field_type.lower() and isinstance(
field_value, MissingDefault
)
field_value = field_value if not isinstance(field_value, MissingDefault) else None
if not field_required:
field_type = extract_type_from_optional(field_type)
@ -245,7 +253,7 @@ def add_extra_fields(frontend_node, field_config, function_args):
def get_field_dict(field: Union[TemplateField, dict]):
"""Get the field dictionary from a TemplateField or a dict"""
if isinstance(field, TemplateField):
return field.model_dump(by_alias=True, exclude_none=True)
return dotdict(field.model_dump(by_alias=True, exclude_none=True))
return field
@ -284,6 +292,7 @@ def run_build_config(
# Allow user to build TemplateField as well
# as a dict with the same keys as TemplateField
field_dict = get_field_dict(field)
build_config[field_name] = field_dict
# This has to be done to set refresh if options or value are callable
if update_field is not None and field_name != update_field:
build_config = update_field_dict(
@ -320,7 +329,11 @@ def run_build_config(
return build_config, custom_instance
except Exception as exc:
logger.error(f"Error while building field config: {str(exc)}")
if hasattr(exc, "detail") and "traceback" in exc.detail:
logger.error(exc.detail["traceback"])
raise exc
@ -345,6 +358,7 @@ def build_frontend_node(template_config):
def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_config):
code_field = TemplateField(
dynamic=True,
required=True,
@ -353,7 +367,7 @@ def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_c
value=raw_code,
password=False,
name="code",
advanced=field_config.pop("advanced", False),
advanced=True,
field_type="code",
is_list=False,
)
@ -404,7 +418,7 @@ def build_custom_component_template(
status_code=400,
detail={
"error": (
"Invalid type convertion. Please check your code and try again."
f"Something went wrong while building the custom component. Hints: {str(exc)}"
),
"traceback": traceback.format_exc(),
},
@ -415,7 +429,6 @@ def create_component_template(component):
"""Create a template for a component."""
component_code = component["code"]
component_output_types = component["output_types"]
# remove
component_extractor = CustomComponent(code=component_code)
@ -431,9 +444,7 @@ def build_custom_components(components_paths: List[str]):
if not components_paths:
return {}
logger.info(
f"Building custom components from {components_paths}"
)
logger.info(f"Building custom components from {components_paths}")
custom_components_from_file = {}
processed_paths = set()
for path in components_paths:
@ -464,18 +475,24 @@ def update_field_dict(
call: bool = False,
):
"""Update the field dictionary by calling options() or value() if they are callable"""
if "refresh" in field_dict:
if ("real_time_refresh" in field_dict or "refresh_button" in field_dict) and any(
(
field_dict.get("real_time_refresh", False),
field_dict.get("refresh_button", False),
)
):
if call:
try:
dd_build_config = dotdict(build_config)
custom_component_instance.update_build_config(
build_config, update_field, update_field_value
dd_build_config, update_field, update_field_value
)
build_config = dd_build_config
except Exception as exc:
logger.error(f"Error while running update_build_config: {str(exc)}")
raise UpdateBuildConfigError(
f"Error while running update_build_config: {str(exc)}"
) from exc
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):
@ -483,8 +500,10 @@ def update_field_dict(
return build_config
def sanitize_field_config(field_config: Dict):
def sanitize_field_config(field_config: Union[Dict, TemplateField]):
# If any of the already existing keys are in field_config, remove them
if isinstance(field_config, TemplateField):
field_config = field_config.to_dict()
for key in [
"name",
"field_type",

View file

@ -72,3 +72,17 @@ def get_all_types_dict(components_paths):
return merge_nested_dicts_with_renaming(
native_components, custom_components_from_file
)
def get_all_components(components_paths, as_dict=False):
"""Get all components names combining native and custom components."""
all_types_dict = get_all_types_dict(components_paths)
components = [] if not as_dict else {}
for category in all_types_dict.values():
for component in category.values():
component["name"] = component["display_name"]
if as_dict:
components[component["name"]] = component
else:
components.append(component)
return components

View file

@ -1,3 +1,4 @@
from .dotdict import dotdict
from .schema import Record
__all__ = ["Record"]
__all__ = ["Record", "dotdict"]

View file

@ -0,0 +1,71 @@
class dotdict(dict):
"""
dotdict allows accessing dictionary elements using dot notation (e.g., dict.key instead of dict['key']).
It automatically converts nested dictionaries into dotdict instances, enabling dot notation on them as well.
Note:
- Only keys that are valid attribute names (e.g., strings that could be variable names) are accessible via dot notation.
- Keys which are not valid Python attribute names or collide with the dict method names (like 'items', 'keys')
should be accessed using the traditional dict['key'] notation.
"""
def __getattr__(self, attr):
"""
Override dot access to behave like dictionary lookup. Automatically convert nested dicts to dotdicts.
Args:
attr (str): Attribute to access.
Returns:
The value associated with 'attr' in the dictionary, converted to dotdict if it is a dict.
Raises:
AttributeError: If the attribute is not found in the dictionary.
"""
try:
value = self[attr]
if isinstance(value, dict) and not isinstance(value, dotdict):
value = dotdict(value)
self[attr] = value # Update self to nest dotdict for future accesses
return value
except KeyError:
raise AttributeError(f"'dotdict' object has no attribute '{attr}'")
def __setattr__(self, key, value):
"""
Override attribute setting to work as dictionary item assignment.
Args:
key (str): The key under which to store the value.
value: The value to store in the dictionary.
"""
if isinstance(value, dict) and not isinstance(value, dotdict):
value = dotdict(value)
self[key] = value
def __delattr__(self, key):
"""
Override attribute deletion to work as dictionary item deletion.
Args:
key (str): The key of the item to delete from the dictionary.
Raises:
AttributeError: If the key is not found in the dictionary.
"""
try:
del self[key]
except KeyError:
raise AttributeError(f"'dotdict' object has no attribute '{key}'")
def __missing__(self, key):
"""
Handle missing keys by returning an empty dotdict. This allows chaining access without raising KeyError.
Args:
key: The missing key.
Returns:
An empty dotdict instance for the given missing key.
"""
return dotdict()

View file

@ -1,6 +1,6 @@
import copy
from langchain_core.documents import Document # Assumed import
from langchain_core.documents import Document
from pydantic import BaseModel

View file

@ -65,10 +65,17 @@ class TemplateField(BaseModel):
info: Optional[str] = ""
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string."""
refresh: Optional[bool] = None
"""Specifies if the field should be refreshed. Defaults to False."""
real_time_refresh: Optional[bool] = None
"""Specifies if the field should have real time refresh. `refresh_button` must be False. Defaults to None."""
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
refresh_button: Optional[bool] = None
"""Specifies if the field should have a refresh button. Defaults to False."""
refresh_button_text: Optional[str] = None
"""Specifies the text for the refresh button. Defaults to None."""
range_spec: Optional[RangeSpec] = Field(
default=None, serialization_alias="rangeSpec"
)
"""Range specification for the field. Defaults to None."""
title_case: bool = False
@ -117,6 +124,10 @@ class TemplateField(BaseModel):
if not isinstance(value, list):
raise ValueError("file_types must be a list")
return [
(f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type)
(
f".{file_type}"
if isinstance(file_type, str) and not file_type.startswith(".")
else file_type
)
for file_type in value
]

View file

@ -88,10 +88,36 @@ export default function ParameterComponent({
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
};
useEffect(() => {
async function fetchData() {
if (
data.node?.template[name]?.refresh &&
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
@ -128,7 +154,8 @@ export default function ParameterComponent({
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].refresh &&
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
data.node!.template[name].value !== newValue;
data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input
@ -154,7 +181,7 @@ export default function ParameterComponent({
...newNode.data,
};
if (data.node?.template[name].refresh && newTemplate) {
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else newNode.data.node.template[name].value = newValue;
@ -458,7 +485,7 @@ export default function ParameterComponent({
}
onChange={handleOnNewValue}
/>
{/* {data.node?.template[name].refresh && (
{/* {data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
@ -466,26 +493,47 @@ export default function ParameterComponent({
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)} */}
</div>
) : data.node?.template[name].multiline ? (
<TextAreaComponent
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
/>
<div className="mt-2 flex w-full flex-col ">
<div className="flex-grow">
<TextAreaComponent
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
/>
</div>
{data.node?.template[name].refresh_button && (
<div className="flex-grow">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
) : (
<div className="mt-2 flex w-full items-center">
<div
className={
"flex-grow " +
(data.node?.template[name].refresh ? "w-5/6" : "")
(data.node?.template[name].refresh_button ? "w-5/6" : "")
}
>
<InputComponent
@ -496,15 +544,19 @@ export default function ParameterComponent({
onChange={handleOnNewValue}
/>
</div>
{data.node?.template[name].refresh && (
{data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
@ -535,7 +587,7 @@ export default function ParameterComponent({
) : left === true &&
type === "str" &&
(data.node?.template[name].options ||
data.node?.template[name]?.refresh) ? (
data.node?.template[name]?.real_time_refresh) ? (
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
@ -548,15 +600,18 @@ export default function ParameterComponent({
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name].refresh && (
{data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ?? "Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>

View file

@ -1,51 +1,53 @@
import dynamicIconImports from "lucide-react/dynamicIconImports";
import { Suspense, forwardRef, lazy } from "react";
import { Suspense, forwardRef, lazy, memo } from "react";
import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
const ForwardedIconComponent = forwardRef(
(
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref
) => {
let TargetIcon = nodeIconsLucide[name];
if (!TargetIcon) {
// check if name exists in dynamicIconImports
if (!dynamicIconImports[name]) {
TargetIcon = nodeIconsLucide["unknown"];
} else TargetIcon = lazy(dynamicIconImports[name]);
}
const ForwardedIconComponent = memo(
forwardRef(
(
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref
) => {
let TargetIcon = nodeIconsLucide[name];
if (!TargetIcon) {
// check if name exists in dynamicIconImports
if (!dynamicIconImports[name]) {
TargetIcon = nodeIconsLucide["unknown"];
} else TargetIcon = lazy(dynamicIconImports[name]);
}
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
if (!TargetIcon) {
return null; // Render nothing until the icon is loaded
if (!TargetIcon) {
return null; // Render nothing until the icon is loaded
}
const fallback = (
<div style={{ background: "#ddd", width: 24, height: 24 }} />
);
return (
<Suspense fallback={fallback}>
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
</Suspense>
);
}
const fallback = (
<div style={{ background: "#ddd", width: 24, height: 24 }} />
);
return (
<Suspense fallback={fallback}>
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
</Suspense>
);
}
)
);
export default ForwardedIconComponent;

View file

@ -7,6 +7,7 @@ function RefreshButton({
isLoading,
disabled,
name,
button_text,
data,
handleUpdateValues,
className,
@ -15,6 +16,7 @@ function RefreshButton({
isLoading: boolean;
disabled: boolean;
name: string;
button_text: string;
data: NodeDataType;
className?: string;
handleUpdateValues: (name: string, data: NodeDataType) => void;
@ -43,6 +45,7 @@ function RefreshButton({
onClick={handleClick}
id={id}
>
<span className="mr-1">{button_text}</span>
<IconComponent
name={isLoading ? "Loader2" : "RefreshCcw"}
className={iconClassName}

View file

@ -1,6 +1,7 @@
// src/constants/constants.ts
import { languageMap } from "../types/components";
import { FlowType } from "../types/flow";
/**
* invalid characters for flow name
@ -739,4 +740,75 @@ export const PRIORITY_SIDEBAR_ORDER = [
"helpers",
"experimental",
];
/*
Data ingestion
Basic Prompting
Chat com memória
Working with data (file/website)
API requests
Vector Store
Assistant
*/
export const EXAMPLES_MOCK:FlowType[] = [
{
name: "Working with data",
id: "Working with data Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 }
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "Basic Prompting",
id: "Basic Prompting Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 }
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "Chat with memory",
id: "Chat with memory Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 }
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "API requests",
id: "API requests Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 }
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
{
name: "Assistant",
id: "Assistant Description",
data: {
nodes: [],
edges: [],
viewport: { zoom: 1, x: 1, y: 1 }
},
description: "This flow represents the first process in our application.",
folder: STARTER_FOLDER_NAME,
user_id: undefined,
},
];

View file

@ -1,12 +1,11 @@
import { cloneDeep } from "lodash";
import { LinkIcon, SparklesIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants";
import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal";
import ShareModal from "../../../../modals/shareModal";
import useAlertStore from "../../../../stores/alertStore";
@ -268,37 +267,37 @@ export default function ExtraSidebar(): JSX.Element {
<>
{index === 0 && (
<div className="pt-0.5">
<div className="p-2 px-4 font-semibold" key={index}>
Native
<div className="p-2 px-4 font-semibold text-sm" key={index}>
Native Components
</div>
</div>
)}
{index === PRIORITY_SIDEBAR_ORDER.length - 1 && (
<>
<a target={"_blank"} href="https://langflow.store" className="components-disclosure-arrangement">
<a
target={"_blank"}
href="https://langflow.store"
className="components-disclosure-arrangement"
>
<div className="flex gap-4">
{/* BUG ON THIS ICON */}
<IconComponent
<SparklesIcon
strokeWidth={1.5}
name="sparkles"
className="text-primary"
className="text-primary w-[22px]"
/>
<span className="components-disclosure-title">
Discover More
</span>
</div>
<div className="components-disclosure-div">
<div>
<IconComponent
name="link"
className={`
h-4 w-4 text-foreground`}
/>
<LinkIcon className="h-4 w-4 text-foreground" />
</div>
</div>
</a>
<div className="p-2 px-4 font-semibold" key={index}>
Legacy
<div className="p-2 px-4 font-semibold text-sm" key={index}>
Legacy Components
</div>
</>
)}

View file

@ -10,6 +10,7 @@ import SidebarNav from "../../components/sidebarComponent";
import { Button } from "../../components/ui/button";
import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants";
import {
EXAMPLES_MOCK,
MY_COLLECTION_DESC,
USER_PROJECTS_HEADER,
} from "../../constants/constants";
@ -133,7 +134,7 @@ export default function HomePage(): JSX.Element {
</BaseModal.Header>
<BaseModal.Content>
<div className="flex h-full w-full flex-wrap gap-3 overflow-auto p-4 custom-scroll">
{examples.map((example, idx) => {
{EXAMPLES_MOCK.map((example, idx) => {
return <UndrawCardComponent key={idx} flow={example} />;
})}
<NewFlowCardComponent />

View file

@ -69,7 +69,7 @@
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
}
.side-bar-search-div-placement {
@apply relative mx-auto flex items-center py-5;
@apply relative mx-auto flex items-center py-3;
}
.side-bar-components-icon {
@apply h-6 w-4 text-ring;

View file

@ -55,7 +55,9 @@ export type TemplateVariableType = {
input_types?: Array<string>;
display_name?: string;
name?: string;
refresh?: boolean;
real_time_refresh?: boolean;
refresh_button?: boolean;
refresh_button_text?: string;
[key: string]: any;
};
export type sendAllProps = {

View file

@ -0,0 +1,84 @@
from itertools import chain
import pytest
from sqlalchemy import func
from sqlmodel import select
from langflow.graph.graph.base import Graph
from langflow.graph.schema import ResultData
from langflow.initial_setup.setup import (
STARTER_FOLDER_NAME,
create_or_update_starter_projects,
get_project_data,
load_starter_projects,
)
from langflow.services.database.models.flow.model import Flow
from langflow.services.deps import session_scope
def test_load_starter_projects():
projects = load_starter_projects()
assert isinstance(projects, list)
assert all(isinstance(project, dict) for project in projects)
def test_get_project_data():
projects = load_starter_projects()
for project in projects:
data = get_project_data(project)
assert all(d is not None for d in data)
def test_create_or_update_starter_projects(client):
with session_scope() as session:
# Run the function to create or update projects
create_or_update_starter_projects()
# Get the number of projects returned by load_starter_projects
num_projects = len(load_starter_projects())
# Get the number of projects in the database
num_db_projects = session.exec(
select(func.count(Flow.id)).where(Flow.folder == STARTER_FOLDER_NAME)
).one()
# Check that the number of projects in the database is the same as the number of projects returned by load_starter_projects
assert num_db_projects == num_projects
@pytest.mark.asyncio
async def test_starter_project_can_run_successfully(client):
with session_scope() as session:
# Run the function to create or update projects
create_or_update_starter_projects()
# Get the number of projects returned by load_starter_projects
num_projects = len(load_starter_projects())
# Get the number of projects in the database
num_db_projects = session.exec(
select(func.count(Flow.id)).where(Flow.folder == STARTER_FOLDER_NAME)
).one()
# Check that the number of projects in the database is the same as the number of projects returned by load_starter_projects
assert num_db_projects == num_projects
# Get all the starter projects
projects = session.exec(
select(Flow).where(Flow.folder == STARTER_FOLDER_NAME)
).all()
graphs: list[Graph] = [
(project.name, Graph.from_payload(project.data, flow_id=project.id))
for project in projects
]
assert len(graphs) == len(projects)
for name, graph in graphs:
outputs = await graph.run(
inputs={},
outputs=[],
session_id="test",
)
assert all(
isinstance(output, ResultData) for output in chain.from_iterable(outputs)
), f"Project {name} error: {outputs}"