Merge branch 'zustand/io/migration' of github.com:logspace-ai/langflow into zustand/io/migration
This commit is contained in:
commit
e120bfe869
44 changed files with 813 additions and 314 deletions
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}.")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from .dotdict import dotdict
|
||||
from .schema import Record
|
||||
|
||||
__all__ = ["Record"]
|
||||
__all__ = ["Record", "dotdict"]
|
||||
|
|
|
|||
71
src/backend/langflow/schema/dotdict.py
Normal file
71
src/backend/langflow/schema/dotdict.py
Normal 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()
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import copy
|
||||
|
||||
from langchain_core.documents import Document # Assumed import
|
||||
from langchain_core.documents import Document
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
84
tests/test_initial_setup.py
Normal file
84
tests/test_initial_setup.py
Normal 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}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue