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_. | | _`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]`_. | | _`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. | | _`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"> <Admonition type="info" label="Tip">

View file

@ -1,9 +1,7 @@
from typing import Optional from typing import Optional
from langchain.prompts import PromptTemplate
from pydantic import BaseModel, field_validator, model_serializer 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 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): def is_json_like(var):
if var.startswith("{{") and var.endswith("}}"): if var.startswith("{{") and var.endswith("}}"):
# If it is a double brance variable # 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 # Handle variables starting with a number
if var[0].isdigit(): if var[0].isdigit():
invalid_chars.append(var[0]) 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 # Temporarily replace {{ and }} to avoid treating them as invalid
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ") 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 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): if any(var for var in input_variables if var not in fixed_variables):
error_message = ( error_message = (
f"Error: Input variables contain invalid characters or formats. \n" 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): if is_json_like(var):
continue continue
new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables) new_var, wrong_variables, empty_variables = fix_variable(
wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables) var, invalid_chars, wrong_variables
)
wrong_variables, empty_variables = check_variable(
var, INVALID_CHARACTERS, wrong_variables, empty_variables
)
fixed_variables.append(new_var) fixed_variables.append(new_var)
variables_to_check.append(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 return fixed_variables

View file

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

View file

@ -6,9 +6,14 @@ from langflow.api.v1.base import (
CodeValidationResponse, CodeValidationResponse,
PromptValidationResponse, PromptValidationResponse,
ValidatePromptRequest, 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, validate_prompt,
) )
from langflow.template.field.prompt import DefaultPromptField
from langflow.utils.validate import validate_code from langflow.utils.validate import validate_code
# build router # build router
@ -37,13 +42,28 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
input_variables=input_variables, input_variables=input_variables,
frontend_node=None, 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( return PromptValidationResponse(
input_variables=input_variables, input_variables=input_variables,
@ -52,61 +72,3 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from 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 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.schema import Record
from langflow.template.field.prompt import DefaultPromptField
def dict_values_to_string(d: dict) -> dict: 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. str: The document as a string.
""" """
return document.page_content 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 asyncio
import json
from typing import List, Optional from typing import List, Optional
import httpx import httpx
import json
from langflow import CustomComponent from langflow import CustomComponent
from langflow.schema import Record from langflow.schema import Record
from langflow.services.database.models.base import orjson_dumps
class APIRequest(CustomComponent): class APIRequest(CustomComponent):
@ -52,35 +51,44 @@ class APIRequest(CustomComponent):
timeout: int = 5, timeout: int = 5,
) -> Record: ) -> Record:
method = method.upper() 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}") raise ValueError(f"Unsupported method: {method}")
data = body if body else None data = body if body else None
data = json.dumps(data) data = json.dumps(data)
try: 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: try:
response_json = response.json() result = response.json()
result = orjson_dumps(response_json, indent_2=False)
except Exception: except Exception:
result = response.text result = response.text
return Record( return Record(
text=result,
data={ data={
"source": url, "source": url,
"headers": headers, "headers": headers,
"status_code": response.status_code, "status_code": response.status_code,
"result": result,
}, },
) )
except httpx.TimeoutException: except httpx.TimeoutException:
return Record( return Record(
text="Request Timed Out", data={
data={"source": url, "headers": headers, "status_code": 408}, "source": url,
"headers": headers,
"status_code": 408,
"error": "Request timed out",
},
) )
except Exception as exc: except Exception as exc:
return Record( return Record(
text=str(exc), data={
data={"source": url, "headers": headers, "status_code": 500}, "source": url,
"headers": headers,
"status_code": 500,
"error": str(exc),
},
) )
async def build( async def build(
@ -88,15 +96,23 @@ class APIRequest(CustomComponent):
method: str, method: str,
url: List[str], url: List[str],
headers: Optional[dict] = None, headers: Optional[dict] = None,
body: Optional[dict] = None, body: Optional[List[Record]] = None,
timeout: int = 5, timeout: int = 5,
) -> List[Record]: ) -> List[Record]:
if headers is None: if headers is None:
headers = {} headers = {}
urls = url if isinstance(url, list) else [url] 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: async with httpx.AsyncClient() as client:
results = await asyncio.gather( 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 return results

View file

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

View file

@ -9,7 +9,9 @@ class UUIDGeneratorComponent(CustomComponent):
display_name = "Unique ID Generator" display_name = "Unique ID Generator"
description = "Generates a unique ID." 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": if field_name == "unique_id":
build_config[field_name]["value"] = str(uuid.uuid4()) build_config[field_name]["value"] = str(uuid.uuid4())
return build_config return build_config
@ -18,7 +20,7 @@ class UUIDGeneratorComponent(CustomComponent):
return { return {
"unique_id": { "unique_id": {
"display_name": "Value", "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" display_name: str = "Prompt"
description: str = "A component for creating prompts using templates" description: str = "A component for creating prompts using templates"
beta = True beta = True
icon = "terminal-square"
def build_config(self): def build_config(self):
return { return {

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ from langflow.field_typing import Text
class QianfanChatEndpointComponent(LCModelComponent): class QianfanChatEndpointComponent(LCModelComponent):
display_name: str = "QianfanChat Model" display_name: str = "QianfanChat"
description: str = ( description: str = (
"Generate text using Baidu Qianfan chat models. Get more detail from " "Generate text using Baidu Qianfan chat models. Get more detail from "
"https://python.langchain.com/docs/integrations/chat/baidu_qianfan_endpoint." "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): class CTransformersComponent(LCModelComponent):
display_name = "CTransformersModel" display_name = "CTransformers"
description = "Generate text using CTransformers LLM models" description = "Generate text using CTransformers LLM models"
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/ctransformers" 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): class CohereComponent(LCModelComponent):
display_name = "CohereModel" display_name = "Cohere"
description = "Generate text using Cohere large language models." description = "Generate text using Cohere large language models."
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/cohere" 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): 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." description: str = "Generate text using Google Generative AI to generate text."
icon = "GoogleGenerativeAI" icon = "GoogleGenerativeAI"
icon = "Google" icon = "Google"

View file

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

View file

@ -7,7 +7,7 @@ from langflow.field_typing import Text
class LlamaCppComponent(LCModelComponent): class LlamaCppComponent(LCModelComponent):
display_name = "LlamaCppModel" display_name = "LlamaCpp"
description = "Generate text using llama.cpp model." description = "Generate text using llama.cpp model."
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/llamacpp" 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): class ChatOllamaComponent(LCModelComponent):
display_name = "ChatOllamaModel" display_name = "ChatOllama"
description = "Generate text using Local LLM for chat with Ollama." description = "Generate text using Local LLM for chat with Ollama."
icon = "Ollama" icon = "Ollama"

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@ from loguru import logger
from langflow.graph.schema import ( from langflow.graph.schema import (
INPUT_COMPONENTS, INPUT_COMPONENTS,
INPUT_FIELD_NAME,
OUTPUT_COMPONENTS, OUTPUT_COMPONENTS,
InterfaceComponentTypes, InterfaceComponentTypes,
ResultData, ResultData,
@ -709,7 +710,8 @@ class Vertex:
self._reset() self._reset()
if self._is_chat_input() and inputs is not None: 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 # Run steps
for step in self.steps: for step in self.steps:

View file

@ -6,8 +6,9 @@ from emoji import demojize, purely_emoji
from loguru import logger from loguru import logger
from sqlmodel import select 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.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" 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. # 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(): def load_starter_projects():
starter_projects = [] starter_projects = []
folder = Path(__file__).parent / "starter_projects" folder = Path(__file__).parent / "starter_projects"
@ -115,6 +133,8 @@ def delete_start_projects(session):
def create_or_update_starter_projects(): 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: with session_scope() as session:
starter_projects = load_starter_projects() starter_projects = load_starter_projects()
delete_start_projects(session) delete_start_projects(session)
@ -128,6 +148,9 @@ def create_or_update_starter_projects():
project_icon, project_icon,
project_icon_bg_color, project_icon_bg_color,
) = get_project_data(project) ) = get_project_data(project)
project_data = update_projects_components_with_latest_component_versions(
project_data, all_types_dict
)
if project_name and project_data: if project_name and project_data:
for existing_project in get_all_flows_similar_to_project( for existing_project in get_all_flows_similar_to_project(
session, project_name session, project_name

View file

@ -9,7 +9,11 @@ from fastapi import HTTPException
from loguru import logger from loguru import logger
from langflow.interface.custom.eval import eval_custom_component_code 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): class CodeSyntaxError(HTTPException):
@ -95,7 +99,9 @@ class CodeParser:
elif isinstance(node, ast.ImportFrom): elif isinstance(node, ast.ImportFrom):
for alias in node.names: for alias in node.names:
if alias.asname: 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: else:
self.data["imports"].append((node.module, alias.name)) self.data["imports"].append((node.module, alias.name))
@ -144,7 +150,9 @@ class CodeParser:
return_type = None return_type = None
if node.returns: if node.returns:
return_type_str = ast.unparse(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: try:
return_type = eval(return_type_str, eval_env) return_type = eval(return_type_str, eval_env)
@ -185,15 +193,23 @@ class CodeParser:
num_args = len(node.args.args) num_args = len(node.args.args)
num_defaults = len(node.args.defaults) num_defaults = len(node.args.defaults)
num_missing_defaults = num_args - num_defaults num_missing_defaults = num_args - num_defaults
missing_defaults = [None] * num_missing_defaults missing_defaults = [MissingDefault()] * num_missing_defaults
default_values = [ast.unparse(default).strip("'") if default else None for default in node.args.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 # Now check all default values to see if there
# are any "None" values in the middle # 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 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 return args
def parse_varargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: 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. Parses the keyword-only arguments of a function or method node.
""" """
kw_defaults = [None] * (len(node.args.kwonlyargs) - len(node.args.kw_defaults)) + [ kw_defaults = [None] * (
ast.unparse(default) if default else None for default in node.args.kw_defaults 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 return args
def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]: def parse_kwargs(self, node: ast.FunctionDef) -> List[Dict[str, Any]]:
@ -319,7 +341,9 @@ class CodeParser:
Extracts global variables from the code. Extracts global variables from the code.
""" """
global_var = { 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), "value": ast.unparse(node.value),
} }
self.data["global_vars"].append(global_var) 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.interface.custom.custom_component.component import Component
from langflow.schema import Record from langflow.schema import Record
from langflow.schema.dotdict import dotdict
from langflow.services.database.models.flow import Flow from langflow.services.database.models.flow import Flow
from langflow.services.database.utils import session_getter from langflow.services.database.utils import session_getter
from langflow.services.deps import ( from langflow.services.deps import (
@ -77,13 +78,17 @@ class CustomComponent(Component):
def update_state(self, name: str, value: Any): def update_state(self, name: str, value: Any):
try: 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: except Exception as e:
raise ValueError(f"Error updating state: {e}") raise ValueError(f"Error updating state: {e}")
def append_state(self, name: str, value: Any): def append_state(self, name: str, value: Any):
try: 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: except Exception as e:
raise ValueError(f"Error appending state: {e}") raise ValueError(f"Error appending state: {e}")
@ -134,7 +139,12 @@ class CustomComponent(Component):
def build_config(self): def build_config(self):
return self.field_config 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 build_config[field_name] = field_value
return build_config return build_config
@ -142,7 +152,9 @@ class CustomComponent(Component):
def tree(self): def tree(self):
return self.get_code_tree(self.code or "") 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. Converts input data into a list of Record objects.
@ -191,7 +203,9 @@ class CustomComponent(Component):
return records 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. Create references from a list of records.
@ -230,14 +244,20 @@ class CustomComponent(Component):
if not self.code: if not self.code:
return {} 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: if not component_classes:
return {} return {}
# Assume the first Component class is the one we're interested in # Assume the first Component class is the one we're interested in
component_class = component_classes[0] component_class = component_classes[0]
build_methods = [ 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 {} 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 # Retrieve and decrypt the credential by name for the current user
db_service = get_db_service() db_service = get_db_service()
with session_getter(db_service) as session: 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 return get_credential
@ -304,7 +326,9 @@ class CustomComponent(Component):
credential_service = get_credential_service() credential_service = get_credential_service()
db_service = get_db_service() db_service = get_db_service()
with session_getter(db_service) as session: 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): def index(self, value: int = 0):
"""Returns a function that returns the value at the given index in the iterable.""" """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: if not self._flows_records:
self.list_flows() self.list_flows()
if not flow_id and self._flows_records: 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: if not flow_ids:
raise ValueError(f"Flow {flow_name} not found") raise ValueError(f"Flow {flow_name} not found")
elif len(flow_ids) > 1: elif len(flow_ids) > 1:
@ -365,7 +393,9 @@ class CustomComponent(Component):
db_service = get_db_service() db_service = get_db_service()
with get_session(db_service) as session: with get_session(db_service) as session:
flows = session.exec( 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() ).all()
flows_records = [flow.to_record() for flow in flows] flows_records = [flow.to_record() for flow in flows]

View file

@ -27,3 +27,12 @@ class CallableCodeDetails(BaseModel):
body: list body: list
return_type: Optional[Any] = None return_type: Optional[Any] = None
has_return: bool = False 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, merge_nested_dicts_with_renaming,
) )
from langflow.interface.custom.eval import eval_custom_component_code 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.field.base import TemplateField
from langflow.template.frontend_node.custom_components import ( from langflow.template.frontend_node.custom_components import (
CustomComponentFrontendNode, 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. str: The extracted type, or an empty string if no type was found.
""" """
match = re.search(r"\[(.*?)\]$", field_type) 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): def get_field_properties(extra_field):
@ -118,7 +120,13 @@ def get_field_properties(extra_field):
field_name = extra_field["name"] field_name = extra_field["name"]
field_type = extra_field.get("type", "str") field_type = extra_field.get("type", "str")
field_value = extra_field.get("default", "") 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: if not field_required:
field_type = extract_type_from_optional(field_type) 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]): def get_field_dict(field: Union[TemplateField, dict]):
"""Get the field dictionary from a TemplateField or a dict""" """Get the field dictionary from a TemplateField or a dict"""
if isinstance(field, TemplateField): 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 return field
@ -284,6 +292,7 @@ def run_build_config(
# Allow user to build TemplateField as well # Allow user to build TemplateField as well
# as a dict with the same keys as TemplateField # as a dict with the same keys as TemplateField
field_dict = get_field_dict(field) 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 # 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: if update_field is not None and field_name != update_field:
build_config = update_field_dict( build_config = update_field_dict(
@ -320,7 +329,11 @@ def run_build_config(
return build_config, custom_instance return build_config, custom_instance
except Exception as exc: except Exception as exc:
logger.error(f"Error while building field config: {str(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 raise exc
@ -345,6 +358,7 @@ def build_frontend_node(template_config):
def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_config): def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_config):
code_field = TemplateField( code_field = TemplateField(
dynamic=True, dynamic=True,
required=True, required=True,
@ -353,7 +367,7 @@ def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_c
value=raw_code, value=raw_code,
password=False, password=False,
name="code", name="code",
advanced=field_config.pop("advanced", False), advanced=True,
field_type="code", field_type="code",
is_list=False, is_list=False,
) )
@ -404,7 +418,7 @@ def build_custom_component_template(
status_code=400, status_code=400,
detail={ detail={
"error": ( "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(), "traceback": traceback.format_exc(),
}, },
@ -415,7 +429,6 @@ def create_component_template(component):
"""Create a template for a component.""" """Create a template for a component."""
component_code = component["code"] component_code = component["code"]
component_output_types = component["output_types"] component_output_types = component["output_types"]
# remove
component_extractor = CustomComponent(code=component_code) component_extractor = CustomComponent(code=component_code)
@ -431,9 +444,7 @@ def build_custom_components(components_paths: List[str]):
if not components_paths: if not components_paths:
return {} return {}
logger.info( logger.info(f"Building custom components from {components_paths}")
f"Building custom components from {components_paths}"
)
custom_components_from_file = {} custom_components_from_file = {}
processed_paths = set() processed_paths = set()
for path in components_paths: for path in components_paths:
@ -464,18 +475,24 @@ def update_field_dict(
call: bool = False, call: bool = False,
): ):
"""Update the field dictionary by calling options() or value() if they are callable""" """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: if call:
try: try:
dd_build_config = dotdict(build_config)
custom_component_instance.update_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: except Exception as exc:
logger.error(f"Error while running update_build_config: {str(exc)}") logger.error(f"Error while running update_build_config: {str(exc)}")
raise UpdateBuildConfigError( raise UpdateBuildConfigError(
f"Error while running update_build_config: {str(exc)}" f"Error while running update_build_config: {str(exc)}"
) from exc ) from exc
field_dict["refresh"] = True
# Let's check if "range_spec" is a RangeSpec object # Let's check if "range_spec" is a RangeSpec object
if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec): if "rangeSpec" in field_dict and isinstance(field_dict["rangeSpec"], RangeSpec):
@ -483,8 +500,10 @@ def update_field_dict(
return build_config 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 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 [ for key in [
"name", "name",
"field_type", "field_type",

View file

@ -72,3 +72,17 @@ def get_all_types_dict(components_paths):
return merge_nested_dicts_with_renaming( return merge_nested_dicts_with_renaming(
native_components, custom_components_from_file 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 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 import copy
from langchain_core.documents import Document # Assumed import from langchain_core.documents import Document
from pydantic import BaseModel from pydantic import BaseModel

View file

@ -65,10 +65,17 @@ class TemplateField(BaseModel):
info: Optional[str] = "" info: Optional[str] = ""
"""Additional information about the field to be shown in the tooltip. Defaults to an empty string.""" """Additional information about the field to be shown in the tooltip. Defaults to an empty string."""
refresh: Optional[bool] = None real_time_refresh: Optional[bool] = None
"""Specifies if the field should be refreshed. Defaults to False.""" """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.""" """Range specification for the field. Defaults to None."""
title_case: bool = False title_case: bool = False
@ -117,6 +124,10 @@ class TemplateField(BaseModel):
if not isinstance(value, list): if not isinstance(value, list):
raise ValueError("file_types must be a list") raise ValueError("file_types must be a list")
return [ 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 for file_type in value
] ]

View file

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

View file

@ -1,51 +1,53 @@
import dynamicIconImports from "lucide-react/dynamicIconImports"; 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 { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils"; import { nodeIconsLucide } from "../../utils/styleUtils";
const ForwardedIconComponent = forwardRef( const ForwardedIconComponent = memo(
( forwardRef(
{ (
name, {
className, name,
iconColor, className,
stroke, iconColor,
strokeWidth, stroke,
id = "", strokeWidth,
}: IconComponentProps, id = "",
ref }: IconComponentProps,
) => { ref
let TargetIcon = nodeIconsLucide[name]; ) => {
if (!TargetIcon) { let TargetIcon = nodeIconsLucide[name];
// check if name exists in dynamicIconImports if (!TargetIcon) {
if (!dynamicIconImports[name]) { // check if name exists in dynamicIconImports
TargetIcon = nodeIconsLucide["unknown"]; if (!dynamicIconImports[name]) {
} else TargetIcon = lazy(dynamicIconImports[name]); TargetIcon = nodeIconsLucide["unknown"];
} } else TargetIcon = lazy(dynamicIconImports[name]);
}
const style = { const style = {
strokeWidth: strokeWidth ?? 1.5, strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }), ...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }), ...(iconColor && { color: iconColor, stroke: stroke }),
}; };
if (!TargetIcon) { if (!TargetIcon) {
return null; // Render nothing until the icon is loaded 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; export default ForwardedIconComponent;

View file

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

View file

@ -1,6 +1,7 @@
// src/constants/constants.ts // src/constants/constants.ts
import { languageMap } from "../types/components"; import { languageMap } from "../types/components";
import { FlowType } from "../types/flow";
/** /**
* invalid characters for flow name * invalid characters for flow name
@ -739,4 +740,75 @@ export const PRIORITY_SIDEBAR_ORDER = [
"helpers", "helpers",
"experimental", "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 { cloneDeep } from "lodash";
import { LinkIcon, SparklesIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input"; import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator"; import { Separator } from "../../../../components/ui/separator";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants"; import { PRIORITY_SIDEBAR_ORDER } from "../../../../constants/constants";
import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal"; import ExportModal from "../../../../modals/exportModal";
import ShareModal from "../../../../modals/shareModal"; import ShareModal from "../../../../modals/shareModal";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
@ -268,37 +267,37 @@ export default function ExtraSidebar(): JSX.Element {
<> <>
{index === 0 && ( {index === 0 && (
<div className="pt-0.5"> <div className="pt-0.5">
<div className="p-2 px-4 font-semibold" key={index}> <div className="p-2 px-4 font-semibold text-sm" key={index}>
Native Native Components
</div> </div>
</div> </div>
)} )}
{index === PRIORITY_SIDEBAR_ORDER.length - 1 && ( {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"> <div className="flex gap-4">
{/* BUG ON THIS ICON */} {/* BUG ON THIS ICON */}
<IconComponent <SparklesIcon
strokeWidth={1.5} strokeWidth={1.5}
name="sparkles" className="text-primary w-[22px]"
className="text-primary"
/> />
<span className="components-disclosure-title"> <span className="components-disclosure-title">
Discover More Discover More
</span> </span>
</div> </div>
<div className="components-disclosure-div"> <div className="components-disclosure-div">
<div> <div>
<IconComponent <LinkIcon className="h-4 w-4 text-foreground" />
name="link"
className={`
h-4 w-4 text-foreground`}
/>
</div> </div>
</div> </div>
</a> </a>
<div className="p-2 px-4 font-semibold" key={index}> <div className="p-2 px-4 font-semibold text-sm" key={index}>
Legacy Legacy Components
</div> </div>
</> </>
)} )}

View file

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

View file

@ -69,7 +69,7 @@
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide; @apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
} }
.side-bar-search-div-placement { .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 { .side-bar-components-icon {
@apply h-6 w-4 text-ring; @apply h-6 w-4 text-ring;

View file

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