Global variables implementation (#1548)
This PR is related to the Global Variables functionality.  When clicking the Globe, it's possible to use global variables in any InputComponent. When hovering an item, it's possible to delete it. When clicking on Add New Variable, it's possible to add a new global variable.
This commit is contained in:
commit
54fb1ba0bb
90 changed files with 1626 additions and 1385 deletions
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
|
|
@ -14,7 +14,7 @@ on:
|
|||
- "src/backend/**"
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.7.0"
|
||||
POETRY_VERSION: "1.8.2"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
|
@ -22,7 +22,6 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
steps:
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -46,7 +46,7 @@ format:
|
|||
|
||||
lint:
|
||||
make install_backend
|
||||
poetry run mypy src/backend
|
||||
poetry run mypy --namespace-packages -p "langflow"
|
||||
poetry run ruff . --fix
|
||||
|
||||
install_frontend:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ ENV PYTHONUNBUFFERED=1 \
|
|||
\
|
||||
# poetry
|
||||
# https://python-poetry.org/docs/configuration/#using-environment-variables
|
||||
POETRY_VERSION=1.7.1 \
|
||||
POETRY_VERSION=1.8.2 \
|
||||
# make poetry install to this location
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
# make poetry create the virtual environment in the project's root
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ ENV PYTHONUNBUFFERED=1 \
|
|||
\
|
||||
# poetry
|
||||
# https://python-poetry.org/docs/configuration/#using-environment-variables
|
||||
POETRY_VERSION=1.7.1 \
|
||||
POETRY_VERSION=1.8.2 \
|
||||
# make poetry install to this location
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
# make poetry create the virtual environment in the project's root
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ ENV PYTHONUNBUFFERED=1 \
|
|||
\
|
||||
# poetry
|
||||
# https://python-poetry.org/docs/configuration/#using-environment-variables
|
||||
POETRY_VERSION=1.5.1 \
|
||||
POETRY_VERSION=1.8.2 \
|
||||
# make poetry install to this location
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
# make poetry create the virtual environment in the project's root
|
||||
|
|
|
|||
|
|
@ -285,7 +285,12 @@ def run_langflow(host, port, log_level, options, app):
|
|||
# MacOS requires an env variable to be set to use gunicorn
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=host, port=port, log_level=log_level)
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level.lower(),
|
||||
)
|
||||
else:
|
||||
from langflow.server import LangflowApplication
|
||||
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, List, Optional, Tuple, Union
|
||||
|
||||
from pydantic.v1 import BaseModel, Field, create_model
|
||||
from sqlmodel import select
|
||||
|
||||
from langflow.schema.schema import INPUT_FIELD_NAME, Record
|
||||
from langflow.services.database.models.flow.model import Flow
|
||||
from langflow.services.deps import session_scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
|
||||
INPUT_TYPE_MAP = {
|
||||
"ChatInput": {"type_hint": "Optional[str]", "default": '""'},
|
||||
"TextInput": {"type_hint": "Optional[str]", "default": '""'},
|
||||
"JSONInput": {"type_hint": "Optional[dict]", "default": "{}"},
|
||||
}
|
||||
|
||||
|
||||
def list_flows(*, user_id: Optional[str] = None) -> List[Record]:
|
||||
if not user_id:
|
||||
raise ValueError("Session is invalid")
|
||||
try:
|
||||
with session_scope() as session:
|
||||
flows = session.exec(
|
||||
select(Flow).where(Flow.user_id == user_id).where(Flow.is_component == False) # noqa
|
||||
).all()
|
||||
|
||||
flows_records = [flow.to_record() for flow in flows]
|
||||
return flows_records
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error listing flows: {e}")
|
||||
|
||||
|
||||
async def load_flow(
|
||||
user_id: str, flow_id: Optional[str] = None, flow_name: Optional[str] = None, tweaks: Optional[dict] = None
|
||||
) -> "Graph":
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.processing.process import process_tweaks
|
||||
|
||||
if not flow_id and not flow_name:
|
||||
raise ValueError("Flow ID or Flow Name is required")
|
||||
if not flow_id and flow_name:
|
||||
flow_id = find_flow(flow_name, user_id)
|
||||
if not flow_id:
|
||||
raise ValueError(f"Flow {flow_name} not found")
|
||||
|
||||
with session_scope() as session:
|
||||
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
|
||||
if not graph_data:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
if tweaks:
|
||||
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
|
||||
graph = Graph.from_payload(graph_data, flow_id=flow_id)
|
||||
return graph
|
||||
|
||||
|
||||
def find_flow(flow_name: str, user_id: str) -> Optional[str]:
|
||||
with session_scope() as session:
|
||||
flow = session.exec(select(Flow).where(Flow.name == flow_name).where(Flow.user_id == user_id)).first()
|
||||
return flow.id if flow else None
|
||||
|
||||
|
||||
async def run_flow(
|
||||
inputs: Union[dict, List[dict]] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
flow_id: Optional[str] = None,
|
||||
flow_name: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> Any:
|
||||
graph = await load_flow(user_id, flow_id, flow_name, tweaks)
|
||||
|
||||
if inputs is None:
|
||||
inputs = []
|
||||
inputs_list = []
|
||||
inputs_components = []
|
||||
types = []
|
||||
for input_dict in inputs:
|
||||
inputs_list.append({INPUT_FIELD_NAME: input_dict.get("input_value")})
|
||||
inputs_components.append(input_dict.get("components", []))
|
||||
types.append(input_dict.get("type", []))
|
||||
|
||||
return await graph.arun(inputs_list, inputs_components=inputs_components, types=types)
|
||||
|
||||
|
||||
def generate_function_for_flow(inputs: List["Vertex"], flow_id: str) -> Coroutine:
|
||||
"""
|
||||
Generate a dynamic flow function based on the given inputs and flow ID.
|
||||
|
||||
Args:
|
||||
inputs (List[Vertex]): The list of input vertices for the flow.
|
||||
flow_id (str): The ID of the flow.
|
||||
|
||||
Returns:
|
||||
Coroutine: The dynamic flow function.
|
||||
|
||||
Raises:
|
||||
None
|
||||
|
||||
Example:
|
||||
inputs = [vertex1, vertex2]
|
||||
flow_id = "my_flow"
|
||||
function = generate_function_for_flow(inputs, flow_id)
|
||||
result = function(input1, input2)
|
||||
"""
|
||||
# Prepare function arguments with type hints and default values
|
||||
args = [
|
||||
f"{input_.display_name.lower().replace(' ', '_')}: {INPUT_TYPE_MAP[input_.base_name]['type_hint']} = {INPUT_TYPE_MAP[input_.base_name]['default']}"
|
||||
for input_ in inputs
|
||||
]
|
||||
|
||||
# Maintain original argument names for constructing the tweaks dictionary
|
||||
original_arg_names = [input_.display_name for input_ in inputs]
|
||||
|
||||
# Prepare a Pythonic, valid function argument string
|
||||
func_args = ", ".join(args)
|
||||
|
||||
# Map original argument names to their corresponding Pythonic variable names in the function
|
||||
arg_mappings = ", ".join(
|
||||
f'"{original_name}": {name}'
|
||||
for original_name, name in zip(original_arg_names, [arg.split(":")[0] for arg in args])
|
||||
)
|
||||
|
||||
func_body = f"""
|
||||
from typing import Optional
|
||||
async def flow_function({func_args}):
|
||||
tweaks = {{ {arg_mappings} }}
|
||||
from langflow.helpers.flow import run_flow
|
||||
from langchain_core.tools import ToolException
|
||||
try:
|
||||
return await run_flow(
|
||||
tweaks={{key: {{'input_value': value}} for key, value in tweaks.items()}},
|
||||
flow_id="{flow_id}",
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolException(f'Error running flow: ' + e)
|
||||
"""
|
||||
|
||||
compiled_func = compile(func_body, "<string>", "exec")
|
||||
local_scope = {}
|
||||
exec(compiled_func, globals(), local_scope)
|
||||
return local_scope["flow_function"]
|
||||
|
||||
|
||||
def build_function_and_schema(flow_record: Record, graph: "Graph") -> Tuple[Callable, BaseModel]:
|
||||
"""
|
||||
Builds a dynamic function and schema for a given flow.
|
||||
|
||||
Args:
|
||||
flow_record (Record): The flow record containing information about the flow.
|
||||
graph (Graph): The graph representing the flow.
|
||||
|
||||
Returns:
|
||||
Tuple[Callable, BaseModel]: A tuple containing the dynamic function and the schema.
|
||||
"""
|
||||
flow_id = flow_record.id
|
||||
inputs = get_flow_inputs(graph)
|
||||
dynamic_flow_function = generate_function_for_flow(inputs, flow_id)
|
||||
schema = build_schema_from_inputs(flow_record.name, inputs)
|
||||
return dynamic_flow_function, schema
|
||||
|
||||
|
||||
def get_flow_inputs(graph: "Graph") -> List["Vertex"]:
|
||||
"""
|
||||
Retrieves the flow inputs from the given graph.
|
||||
|
||||
Args:
|
||||
graph (Graph): The graph object representing the flow.
|
||||
|
||||
Returns:
|
||||
List[Record]: A list of input records, where each record contains the ID, name, and description of the input vertex.
|
||||
"""
|
||||
inputs = []
|
||||
for vertex in graph.vertices:
|
||||
if vertex.is_input:
|
||||
inputs.append(vertex)
|
||||
return inputs
|
||||
|
||||
|
||||
def build_schema_from_inputs(name: str, inputs: List[tuple[str, str, str]]) -> BaseModel:
|
||||
"""
|
||||
Builds a schema from the given inputs.
|
||||
|
||||
Args:
|
||||
name (str): The name of the schema.
|
||||
inputs (List[tuple[str, str, str]]): A list of tuples representing the inputs.
|
||||
Each tuple contains three elements: the input name, the input type, and the input description.
|
||||
|
||||
Returns:
|
||||
BaseModel: The schema model.
|
||||
|
||||
"""
|
||||
fields = {}
|
||||
for input_ in inputs:
|
||||
field_name = input_.display_name.lower().replace(" ", "_")
|
||||
description = input_.description
|
||||
fields[field_name] = (str, Field(default="", description=description))
|
||||
return create_model(name, **fields)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
from langchain_core.documents import Document
|
||||
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
def docs_to_records(documents: list[Document]) -> list[Record]:
|
||||
"""
|
||||
Converts a list of Documents to a list of Records.
|
||||
|
||||
Args:
|
||||
documents (list[Document]): The list of Documents to convert.
|
||||
|
||||
Returns:
|
||||
list[Record]: The converted list of Records.
|
||||
"""
|
||||
return [Record.from_document(document) for document in documents]
|
||||
|
||||
|
||||
def records_to_text(template: str, records: list[Record]) -> str:
|
||||
"""
|
||||
Converts a list of Records to a list of texts.
|
||||
|
||||
Args:
|
||||
records (list[Record]): The list of Records to convert.
|
||||
|
||||
Returns:
|
||||
list[str]: The converted list of texts.
|
||||
"""
|
||||
if isinstance(records, Record):
|
||||
records = [records]
|
||||
# Check if there are any format strings in the template
|
||||
|
||||
formated_records = [template.format(data=record.data, **record.data) for record in records]
|
||||
return "\n".join(formated_records)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
"""Replace Credential table with Variable
|
||||
|
||||
Revision ID: 1a110b568907
|
||||
Revises: 63b9c451fd30
|
||||
Create Date: 2024-03-25 09:40:02.743453
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1a110b568907"
|
||||
down_revision: Union[str, None] = "63b9c451fd30"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" not in table_names:
|
||||
op.create_table(
|
||||
"variable",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("value", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("type", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_variable_user_id"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
if "credential" in table_names:
|
||||
op.drop_table("credential")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "credential" not in table_names:
|
||||
op.create_table(
|
||||
"credential",
|
||||
sa.Column("name", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("value", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("provider", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("user_id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("created_at", sa.DATETIME(), nullable=False),
|
||||
sa.Column("updated_at", sa.DATETIME(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_credential_user_id"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
if "variable" in table_names:
|
||||
op.drop_table("variable")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -4,7 +4,6 @@ from fastapi import APIRouter
|
|||
from langflow.api.v1 import (
|
||||
api_key_router,
|
||||
chat_router,
|
||||
credentials_router,
|
||||
endpoints_router,
|
||||
files_router,
|
||||
flows_router,
|
||||
|
|
@ -13,6 +12,7 @@ from langflow.api.v1 import (
|
|||
store_router,
|
||||
users_router,
|
||||
validate_router,
|
||||
variables_router,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -26,6 +26,6 @@ router.include_router(flows_router)
|
|||
router.include_router(users_router)
|
||||
router.include_router(api_key_router)
|
||||
router.include_router(login_router)
|
||||
router.include_router(credentials_router)
|
||||
router.include_router(variables_router)
|
||||
router.include_router(files_router)
|
||||
router.include_router(monitor_router)
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ def update_template_field(frontend_template, key, value_dict):
|
|||
template_field["value"] = ""
|
||||
template_field["file_path"] = file_path_value
|
||||
|
||||
if "load_from_db" in value_dict and value_dict["load_from_db"]:
|
||||
template_field["load_from_db"] = value_dict["load_from_db"]
|
||||
|
||||
|
||||
def get_file_path_value(file_path):
|
||||
"""Get the file path value if the file exists, else return empty string."""
|
||||
|
|
@ -161,7 +164,7 @@ def get_is_component_from_data(data: dict):
|
|||
|
||||
|
||||
async def check_langflow_version(component: StoreComponentCreate):
|
||||
from langflow import __version__ as current_version
|
||||
from langflow.version.version import __version__ as current_version # type: ignore
|
||||
|
||||
if not component.last_tested_version:
|
||||
component.last_tested_version = current_version
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from langflow.api.v1.api_key import router as api_key_router
|
||||
from langflow.api.v1.chat import router as chat_router
|
||||
from langflow.api.v1.credential import router as credentials_router
|
||||
from langflow.api.v1.endpoints import router as endpoints_router
|
||||
from langflow.api.v1.files import router as files_router
|
||||
from langflow.api.v1.flows import router as flows_router
|
||||
|
|
@ -9,6 +8,7 @@ from langflow.api.v1.monitor import router as monitor_router
|
|||
from langflow.api.v1.store import router as store_router
|
||||
from langflow.api.v1.users import router as users_router
|
||||
from langflow.api.v1.validate import router as validate_router
|
||||
from langflow.api.v1.variable import router as variables_router
|
||||
|
||||
__all__ = [
|
||||
"chat_router",
|
||||
|
|
@ -19,7 +19,7 @@ __all__ = [
|
|||
"users_router",
|
||||
"api_key_router",
|
||||
"login_router",
|
||||
"credentials_router",
|
||||
"variables_router",
|
||||
"monitor_router",
|
||||
"files_router",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.credential import Credential, CredentialCreate, CredentialRead, CredentialUpdate
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
router = APIRouter(prefix="/credentials", tags=["Credentials"])
|
||||
|
||||
|
||||
@router.post("/", response_model=CredentialRead, status_code=201)
|
||||
def create_credential(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
credential: CredentialCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Create a new credential."""
|
||||
try:
|
||||
# check if credential name already exists
|
||||
credential_exists = session.exec(
|
||||
select(Credential).where(Credential.name == credential.name, Credential.user_id == current_user.id)
|
||||
).first()
|
||||
if credential_exists:
|
||||
raise HTTPException(status_code=400, detail="Credential name already exists")
|
||||
|
||||
credential_dict = credential.model_dump()
|
||||
credential_dict["user_id"] = current_user.id
|
||||
|
||||
db_credential = Credential.model_validate(credential_dict)
|
||||
if not db_credential.value:
|
||||
raise HTTPException(status_code=400, detail="Credential value cannot be empty")
|
||||
encrypted = auth_utils.encrypt_api_key(db_credential.value, settings_service=settings_service)
|
||||
db_credential.value = encrypted
|
||||
db_credential.user_id = current_user.id
|
||||
session.add(db_credential)
|
||||
session.commit()
|
||||
session.refresh(db_credential)
|
||||
return db_credential
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/", response_model=list[CredentialRead], status_code=200)
|
||||
def read_credentials(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read all credentials."""
|
||||
try:
|
||||
credentials = session.exec(select(Credential).where(Credential.user_id == current_user.id)).all()
|
||||
return credentials
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.patch("/{credential_id}", response_model=CredentialRead, status_code=200)
|
||||
def update_credential(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
credential_id: UUID,
|
||||
credential: CredentialUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Update a credential."""
|
||||
try:
|
||||
db_credential = session.exec(
|
||||
select(Credential).where(Credential.id == credential_id, Credential.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_credential:
|
||||
raise HTTPException(status_code=404, detail="Credential not found")
|
||||
|
||||
credential_data = credential.model_dump(exclude_unset=True)
|
||||
for key, value in credential_data.items():
|
||||
setattr(db_credential, key, value)
|
||||
db_credential.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
session.refresh(db_credential)
|
||||
return db_credential
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/{credential_id}", response_model=CredentialRead, status_code=200)
|
||||
def delete_credential(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
credential_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Delete a credential."""
|
||||
try:
|
||||
db_credential = session.exec(
|
||||
select(Credential).where(Credential.id == credential_id, Credential.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_credential:
|
||||
raise HTTPException(status_code=404, detail="Credential not found")
|
||||
session.delete(db_credential)
|
||||
session.commit()
|
||||
return db_credential
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
|
@ -239,7 +239,7 @@ async def create_upload_file(
|
|||
# get endpoint to return version of langflow
|
||||
@router.get("/version")
|
||||
def get_version():
|
||||
from langflow.version import __version__
|
||||
from langflow.version import __version__ # type: ignore
|
||||
|
||||
return {"version": __version__}
|
||||
|
||||
|
|
|
|||
113
src/backend/base/langflow/api/v1/variable.py
Normal file
113
src/backend/base/langflow/api/v1/variable.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.database.models.variable import Variable, VariableCreate, VariableRead, VariableUpdate
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
router = APIRouter(prefix="/variables", tags=["Variables"])
|
||||
|
||||
|
||||
@router.post("/", response_model=VariableRead, status_code=201)
|
||||
def create_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable: VariableCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Create a new variable."""
|
||||
try:
|
||||
# check if variable name already exists
|
||||
variable_exists = session.exec(
|
||||
select(Variable).where(
|
||||
Variable.name == variable.name,
|
||||
Variable.user_id == current_user.id,
|
||||
)
|
||||
).first()
|
||||
if variable_exists:
|
||||
raise HTTPException(status_code=400, detail="Variable name already exists")
|
||||
|
||||
variable_dict = variable.model_dump()
|
||||
variable_dict["user_id"] = current_user.id
|
||||
|
||||
db_variable = Variable.model_validate(variable_dict)
|
||||
if not db_variable.value:
|
||||
raise HTTPException(status_code=400, detail="Variable value cannot be empty")
|
||||
encrypted = auth_utils.encrypt_api_key(db_variable.value, settings_service=settings_service)
|
||||
db_variable.value = encrypted
|
||||
db_variable.user_id = current_user.id
|
||||
session.add(db_variable)
|
||||
session.commit()
|
||||
session.refresh(db_variable)
|
||||
return db_variable
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/", response_model=list[VariableRead], status_code=200)
|
||||
def read_variables(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read all variables."""
|
||||
try:
|
||||
variables = session.exec(select(Variable).where(Variable.user_id == current_user.id)).all()
|
||||
return variables
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.patch("/{variable_id}", response_model=VariableRead, status_code=200)
|
||||
def update_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable_id: UUID,
|
||||
variable: VariableUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Update a variable."""
|
||||
try:
|
||||
db_variable = session.exec(
|
||||
select(Variable).where(Variable.id == variable_id, Variable.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_variable:
|
||||
raise HTTPException(status_code=404, detail="Variable not found")
|
||||
|
||||
variable_data = variable.model_dump(exclude_unset=True)
|
||||
for key, value in variable_data.items():
|
||||
setattr(db_variable, key, value)
|
||||
db_variable.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
session.refresh(db_variable)
|
||||
return db_variable
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/{variable_id}", status_code=204)
|
||||
def delete_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Delete a variable."""
|
||||
try:
|
||||
db_variable = session.exec(
|
||||
select(Variable).where(Variable.id == variable_id, Variable.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_variable:
|
||||
raise HTTPException(status_code=404, detail="Variable not found")
|
||||
session.delete(db_variable)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
from typing import List, Union
|
||||
from typing import List, Optional, Union, cast
|
||||
|
||||
from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseMemory, Text, Tool
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class LCAgentComponent(CustomComponent):
|
||||
|
|
@ -38,11 +40,11 @@ class LCAgentComponent(CustomComponent):
|
|||
|
||||
async def run_agent(
|
||||
self,
|
||||
agent: Union[BaseSingleActionAgent, BaseMultiActionAgent, AgentExecutor],
|
||||
agent: Union[Runnable, BaseSingleActionAgent, BaseMultiActionAgent, AgentExecutor],
|
||||
inputs: str,
|
||||
input_variables: list[str],
|
||||
tools: List[Tool],
|
||||
memory: BaseMemory = None,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
handle_parsing_errors: bool = True,
|
||||
output_key: str = "output",
|
||||
) -> Text:
|
||||
|
|
@ -50,7 +52,11 @@ class LCAgentComponent(CustomComponent):
|
|||
runnable = agent
|
||||
else:
|
||||
runnable = AgentExecutor.from_agent_and_tools(
|
||||
agent=agent, tools=tools, verbose=True, memory=memory, handle_parsing_errors=handle_parsing_errors
|
||||
agent=agent, # type: ignore
|
||||
tools=tools,
|
||||
verbose=True,
|
||||
memory=memory,
|
||||
handle_parsing_errors=handle_parsing_errors,
|
||||
)
|
||||
input_dict = {"input": inputs}
|
||||
for var in input_variables:
|
||||
|
|
@ -59,11 +65,11 @@ class LCAgentComponent(CustomComponent):
|
|||
result = await runnable.ainvoke(input_dict)
|
||||
self.status = result
|
||||
if output_key in result:
|
||||
return result.get(output_key)
|
||||
return cast(str, result.get(output_key))
|
||||
elif "output" not in result:
|
||||
if output_key != "output":
|
||||
raise ValueError(f"Output key not found in result. Tried '{output_key}' and 'output'.")
|
||||
else:
|
||||
raise ValueError("Output key not found in result. Tried 'output'.")
|
||||
|
||||
return result.get("output")
|
||||
return cast(str, result.get("output"))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from langchain_core.language_models.chat_models import BaseChatModel
|
||||
from langchain_core.language_models.llms import LLM
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class LCModelComponent(CustomComponent):
|
||||
|
|
@ -34,15 +34,15 @@ class LCModelComponent(CustomComponent):
|
|||
def get_chat_result(
|
||||
self, runnable: BaseChatModel, stream: bool, input_value: str, system_message: Optional[str] = None
|
||||
):
|
||||
messages = []
|
||||
messages: list[Union[HumanMessage, SystemMessage]] = []
|
||||
if system_message:
|
||||
messages.append(SystemMessage(system_message))
|
||||
messages.append(SystemMessage(content=system_message))
|
||||
if input_value:
|
||||
messages.append(HumanMessage(input_value))
|
||||
messages.append(HumanMessage(content=input_value))
|
||||
if stream:
|
||||
result = runnable.stream(messages)
|
||||
return runnable.stream(messages)
|
||||
else:
|
||||
message = runnable.invoke(messages)
|
||||
result = message.content
|
||||
self.status = result
|
||||
return result
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from langchain.agents import create_xml_agent
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
|
@ -69,7 +69,7 @@ class XMLAgentComponent(LCAgentComponent):
|
|||
llm: BaseLLM,
|
||||
tools: List[Tool],
|
||||
prompt: str,
|
||||
memory: BaseMemory = None,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
tool_template: str = "{name}: {description}",
|
||||
handle_parsing_errors: bool = True,
|
||||
) -> Text:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class CohereEmbeddingsComponent(CustomComponent):
|
|||
self,
|
||||
request_timeout: Optional[float] = None,
|
||||
cohere_api_key: str = "",
|
||||
max_retries: Optional[int] = None,
|
||||
max_retries: int = 3,
|
||||
model: str = "embed-english-v2.0",
|
||||
truncate: Optional[str] = None,
|
||||
user_agent: str = "langchain",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from langchain_openai.embeddings.base import OpenAIEmbeddings
|
||||
from pydantic.v1.types import SecretStr
|
||||
|
||||
from langflow.field_typing import NestedDict
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
|
@ -100,8 +99,6 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
if disallowed_special == ["all"]:
|
||||
disallowed_special = "all" # type: ignore
|
||||
|
||||
api_key = SecretStr(openai_api_key) if openai_api_key else None
|
||||
|
||||
return OpenAIEmbeddings(
|
||||
tiktoken_enabled=tiktoken_enable,
|
||||
default_headers=default_headers,
|
||||
|
|
@ -116,7 +113,7 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
model=model,
|
||||
model_kwargs=model_kwargs,
|
||||
base_url=openai_api_base,
|
||||
api_key=api_key,
|
||||
api_key=openai_api_key,
|
||||
openai_api_type=openai_api_type,
|
||||
api_version=openai_api_version,
|
||||
organization=openai_organization,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, List, Optional, Text
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
from loguru import logger
|
||||
|
|
@ -8,6 +8,7 @@ from langflow.field_typing import Tool
|
|||
from langflow.graph.graph.base import Graph
|
||||
from langflow.helpers.flow import build_function_and_schema
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class FlowToolComponent(CustomComponent):
|
||||
|
|
@ -19,7 +20,7 @@ class FlowToolComponent(CustomComponent):
|
|||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def get_flow(self, flow_name: str) -> Optional[Text]:
|
||||
def get_flow(self, flow_name: str) -> Optional[Record]:
|
||||
"""
|
||||
Retrieves a flow by its name.
|
||||
|
||||
|
|
@ -82,4 +83,4 @@ class FlowToolComponent(CustomComponent):
|
|||
description_repr = repr(tool.description).strip("'")
|
||||
args_str = "\n".join([f"- {arg_name}: {arg_data['description']}" for arg_name, arg_data in tool.args.items()])
|
||||
self.status = f"{description_repr}\nArguments:\n{args_str}"
|
||||
return tool
|
||||
return tool # type: ignore
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
from typing import Any, List, Optional, Text, Tuple
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from langflow.helpers.flow import get_flow_inputs
|
||||
from loguru import logger
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.graph.schema import ResultData, RunOutputs
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.schema import Record
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.template.field.base import TemplateField
|
||||
|
|
@ -20,7 +22,7 @@ class SubFlowComponent(CustomComponent):
|
|||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def get_flow(self, flow_name: str) -> Optional[Text]:
|
||||
def get_flow(self, flow_name: str) -> Optional[Record]:
|
||||
flow_records = self.list_flows()
|
||||
for flow_record in flow_records:
|
||||
if flow_record.data["name"] == flow_name:
|
||||
|
|
@ -42,7 +44,7 @@ class SubFlowComponent(CustomComponent):
|
|||
raise ValueError(f"Flow {field_value} not found.")
|
||||
graph = Graph.from_payload(flow_record.data["data"])
|
||||
# Get all inputs from the graph
|
||||
inputs = self.get_flow_inputs(graph)
|
||||
inputs = get_flow_inputs(graph)
|
||||
# Add inputs to the build config
|
||||
build_config = self.add_inputs_to_build_config(inputs, build_config)
|
||||
except Exception as e:
|
||||
|
|
@ -50,21 +52,13 @@ class SubFlowComponent(CustomComponent):
|
|||
|
||||
return build_config
|
||||
|
||||
def get_flow_inputs(self, graph: Graph) -> List[Record]:
|
||||
inputs = []
|
||||
for vertex in graph.vertices:
|
||||
if vertex.is_input:
|
||||
inputs.append((vertex.id, vertex.display_name, vertex.description))
|
||||
logger.debug(inputs)
|
||||
return inputs
|
||||
|
||||
def add_inputs_to_build_config(self, inputs: List[Tuple], build_config: dotdict):
|
||||
def add_inputs_to_build_config(self, inputs: List[Vertex], build_config: dotdict):
|
||||
new_fields: list[TemplateField] = []
|
||||
for input_id, input_display_name, input_description in inputs:
|
||||
for vertex in inputs:
|
||||
field = TemplateField(
|
||||
display_name=input_display_name,
|
||||
name=input_id,
|
||||
info=input_description,
|
||||
display_name=vertex.display_name,
|
||||
name=vertex.id,
|
||||
info=vertex.description,
|
||||
field_type="str",
|
||||
default=None,
|
||||
)
|
||||
|
|
@ -110,12 +104,15 @@ class SubFlowComponent(CustomComponent):
|
|||
tweaks=tweaks,
|
||||
flow_name=flow_name,
|
||||
)
|
||||
if not run_outputs:
|
||||
return []
|
||||
run_output = run_outputs[0]
|
||||
|
||||
records = []
|
||||
for output in run_output.outputs:
|
||||
if output:
|
||||
records.extend(self.build_records_from_result_data(output))
|
||||
if run_output is not None:
|
||||
for output in run_output.outputs:
|
||||
if output:
|
||||
records.extend(self.build_records_from_result_data(output))
|
||||
|
||||
self.status = records
|
||||
logger.debug(records)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from .ClearMessageHistory import ClearMessageHistoryComponent
|
||||
from .ExtractDataFromRecord import ExtractKeyFromRecordComponent
|
||||
from .Listen import GetNotifiedComponent
|
||||
from .Listen import ListenComponent
|
||||
from .ListFlows import ListFlowsComponent
|
||||
from .MergeRecords import MergeRecordsComponent
|
||||
from .Notify import NotifyComponent
|
||||
|
|
@ -11,7 +11,7 @@ from .SQLExecutor import SQLExecutorComponent
|
|||
__all__ = [
|
||||
"ClearMessageHistoryComponent",
|
||||
"ExtractKeyFromRecordComponent",
|
||||
"GetNotifiedComponent",
|
||||
"ListenComponent",
|
||||
"ListFlowsComponent",
|
||||
"MergeRecordsComponent",
|
||||
"MessageHistoryComponent",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from typing import Optional
|
|||
|
||||
from langchain.llms.base import BaseLanguageModel
|
||||
from langchain_openai import AzureChatOpenAI
|
||||
from pydantic.v1 import SecretStr
|
||||
|
||||
from langflow.base.models.model import LCModelComponent
|
||||
from langflow.field_typing import Text
|
||||
|
|
@ -91,21 +90,20 @@ class AzureChatOpenAIComponent(LCModelComponent):
|
|||
azure_endpoint: str,
|
||||
input_value: Text,
|
||||
azure_deployment: str,
|
||||
api_key: str,
|
||||
api_version: str,
|
||||
api_key: Optional[str] = None,
|
||||
system_message: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = 1000,
|
||||
stream: bool = False,
|
||||
) -> BaseLanguageModel:
|
||||
secret_api_key = SecretStr(api_key)
|
||||
try:
|
||||
output = AzureChatOpenAI(
|
||||
model=model,
|
||||
azure_endpoint=azure_endpoint,
|
||||
azure_deployment=azure_deployment,
|
||||
api_version=api_version,
|
||||
api_key=secret_api_key,
|
||||
api_key=api_key,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from .model import LCModelComponent
|
||||
|
||||
__all__ = ["LCModelComponent"]
|
||||
|
|
@ -34,4 +34,4 @@ class SearchApiToolComponent(CustomComponent):
|
|||
tool = SearchAPIRun(api_wrapper=search_api_wrapper)
|
||||
|
||||
self.status = tool
|
||||
return tool
|
||||
return tool # type: ignore
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from langchain_astradb import AstraDBVectorStore
|
||||
from langchain_astradb.utils.astradb import SetupMode
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import Embeddings, VectorStore
|
||||
|
|
@ -85,6 +86,10 @@ class AstraDBVectorStoreComponent(CustomComponent):
|
|||
metadata_indexing_exclude: Optional[List[str]] = None,
|
||||
collection_indexing_policy: Optional[dict] = None,
|
||||
) -> VectorStore:
|
||||
try:
|
||||
setup_mode_value = SetupMode[setup_mode.upper()]
|
||||
except KeyError:
|
||||
raise ValueError(f"Invalid setup mode: {setup_mode}")
|
||||
if inputs:
|
||||
documents = [_input.to_lc_document() for _input in inputs]
|
||||
|
||||
|
|
@ -100,7 +105,7 @@ class AstraDBVectorStoreComponent(CustomComponent):
|
|||
bulk_insert_batch_concurrency=bulk_insert_batch_concurrency,
|
||||
bulk_insert_overwrite_concurrency=bulk_insert_overwrite_concurrency,
|
||||
bulk_delete_concurrency=bulk_delete_concurrency,
|
||||
setup_mode=setup_mode,
|
||||
setup_mode=setup_mode_value,
|
||||
pre_delete_collection=pre_delete_collection,
|
||||
metadata_indexing_include=metadata_indexing_include,
|
||||
metadata_indexing_exclude=metadata_indexing_exclude,
|
||||
|
|
@ -118,7 +123,7 @@ class AstraDBVectorStoreComponent(CustomComponent):
|
|||
bulk_insert_batch_concurrency=bulk_insert_batch_concurrency,
|
||||
bulk_insert_overwrite_concurrency=bulk_insert_overwrite_concurrency,
|
||||
bulk_delete_concurrency=bulk_delete_concurrency,
|
||||
setup_mode=setup_mode,
|
||||
setup_mode=setup_mode_value,
|
||||
pre_delete_collection=pre_delete_collection,
|
||||
metadata_indexing_include=metadata_indexing_include,
|
||||
metadata_indexing_exclude=metadata_indexing_exclude,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from langflow.field_typing import Embeddings, Text
|
|||
from langflow.schema import Record
|
||||
|
||||
|
||||
class AstraDBSearchComponent(AstraDBVectorStoreComponent, LCVectorStoreComponent):
|
||||
class AstraDBSearchComponent(LCVectorStoreComponent):
|
||||
display_name = "AstraDB Search"
|
||||
description = "Searches an existing AstraDB Vector Store"
|
||||
icon = "AstraDB"
|
||||
|
|
@ -76,7 +76,7 @@ class AstraDBSearchComponent(AstraDBVectorStoreComponent, LCVectorStoreComponent
|
|||
self,
|
||||
embedding: Embeddings,
|
||||
collection_name: str,
|
||||
input_value: Optional[Text] = None,
|
||||
input_value: Text,
|
||||
search_type: str = "Similarity",
|
||||
token: Optional[str] = None,
|
||||
api_endpoint: Optional[str] = None,
|
||||
|
|
@ -92,7 +92,7 @@ class AstraDBSearchComponent(AstraDBVectorStoreComponent, LCVectorStoreComponent
|
|||
metadata_indexing_exclude: Optional[List[str]] = None,
|
||||
collection_indexing_policy: Optional[dict] = None,
|
||||
) -> List[Record]:
|
||||
vector_store = super().build(
|
||||
vector_store = AstraDBVectorStoreComponent().build(
|
||||
embedding=embedding,
|
||||
collection_name=collection_name,
|
||||
token=token,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from langflow.field_typing import Embeddings, NestedDict, Text
|
|||
from langflow.schema import Record
|
||||
|
||||
|
||||
class MongoDBAtlasSearchComponent(MongoDBAtlasComponent, LCVectorStoreComponent):
|
||||
class MongoDBAtlasSearchComponent(LCVectorStoreComponent):
|
||||
display_name = "MongoDB Atlas Search"
|
||||
description = "Search a MongoDB Atlas Vector Store for similar documents."
|
||||
|
||||
|
|
@ -37,9 +37,10 @@ class MongoDBAtlasSearchComponent(MongoDBAtlasComponent, LCVectorStoreComponent)
|
|||
search_kwargs: Optional[NestedDict] = None,
|
||||
) -> List[Record]:
|
||||
search_kwargs = search_kwargs or {}
|
||||
vector_store = super().build(
|
||||
connection_string=mongodb_atlas_cluster_uri,
|
||||
namespace=f"{db_name}.{collection_name}",
|
||||
vector_store = MongoDBAtlasComponent().build(
|
||||
mongodb_atlas_cluster_uri=mongodb_atlas_cluster_uri,
|
||||
collection_name=collection_name,
|
||||
db_name=db_name,
|
||||
embedding=embedding,
|
||||
index_name=index_name,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ class RangeSpec(BaseModel):
|
|||
@classmethod
|
||||
def max_must_be_greater_than_min(cls, v, values, **kwargs):
|
||||
if "min" in values.data and v <= values.data["min"]:
|
||||
raise ValueError("max must be greater than min")
|
||||
raise ValueError("Max must be greater than min")
|
||||
return v
|
||||
|
||||
@field_validator("step")
|
||||
@classmethod
|
||||
def step_must_be_positive(cls, v):
|
||||
if v <= 0:
|
||||
raise ValueError("step must be positive")
|
||||
raise ValueError("Step must be positive")
|
||||
return v
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
from collections import defaultdict, deque
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Coroutine, Dict, Generator, List, Optional, Type, Union
|
||||
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Generator, List, Literal, Optional, Type, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
|
@ -201,7 +201,7 @@ class Graph:
|
|||
self,
|
||||
inputs: Dict[str, str],
|
||||
input_components: list[str],
|
||||
input_type: str,
|
||||
input_type: Literal["chat", "text", "json", "any"] | None,
|
||||
outputs: list[str],
|
||||
stream: bool,
|
||||
session_id: str,
|
||||
|
|
@ -236,7 +236,7 @@ class Graph:
|
|||
continue
|
||||
# If the input_type is not any and the input_type is not in the vertex id
|
||||
# Example: input_type = "chat" and vertex.id = "OpenAI-19ddn"
|
||||
elif input_type != "any" and input_type not in vertex.id.lower():
|
||||
elif input_type is not None and input_type != "any" and input_type not in vertex.id.lower():
|
||||
continue
|
||||
if vertex is None:
|
||||
raise ValueError(f"Vertex {vertex_id} not found")
|
||||
|
|
@ -269,9 +269,9 @@ class Graph:
|
|||
|
||||
def run(
|
||||
self,
|
||||
inputs: Dict[str, str],
|
||||
input_components: Optional[list[str]] = None,
|
||||
types: Optional[list[str]] = None,
|
||||
inputs: list[Dict[str, str]],
|
||||
input_components: Optional[list[list[str]]] = None,
|
||||
types: Optional[list[Literal["chat", "text", "json", "any"] | None]] = None,
|
||||
outputs: Optional[list[str]] = None,
|
||||
session_id: Optional[str] = None,
|
||||
stream: bool = False,
|
||||
|
|
@ -309,7 +309,7 @@ class Graph:
|
|||
self,
|
||||
inputs: list[Dict[str, str]],
|
||||
inputs_components: Optional[list[list[str]]] = None,
|
||||
types: Optional[list[str]] = None,
|
||||
types: Optional[list[Literal["chat", "text", "json", "any"] | None]] = None,
|
||||
outputs: Optional[list[str]] = None,
|
||||
session_id: Optional[str] = None,
|
||||
stream: bool = False,
|
||||
|
|
@ -338,8 +338,12 @@ class Graph:
|
|||
inputs = [{}]
|
||||
# Length of all should be the as inputs length
|
||||
# just add empty lists to complete the length
|
||||
if inputs_components is None:
|
||||
inputs_components = []
|
||||
for _ in range(len(inputs) - len(inputs_components)):
|
||||
inputs_components.append([])
|
||||
if types is None:
|
||||
types = []
|
||||
for _ in range(len(inputs) - len(types)):
|
||||
types.append("any")
|
||||
for run_inputs, components, input_type in zip(inputs, inputs_components, types):
|
||||
|
|
@ -650,7 +654,7 @@ class Graph:
|
|||
async def build_vertex(
|
||||
self,
|
||||
lock: asyncio.Lock,
|
||||
set_cache_coro: Coroutine,
|
||||
set_cache_coro: Callable[["Graph", asyncio.Lock], Coroutine],
|
||||
vertex_id: str,
|
||||
inputs_dict: Optional[Dict[str, str]] = None,
|
||||
user_id: Optional[str] = None,
|
||||
|
|
@ -693,7 +697,9 @@ class Graph:
|
|||
logger.exception(f"Error building vertex: {exc}")
|
||||
raise exc
|
||||
|
||||
async def get_next_and_top_level_vertices(self, lock: asyncio.Lock, set_cache_coro: Coroutine, vertex: Vertex):
|
||||
async def get_next_and_top_level_vertices(
|
||||
self, lock: asyncio.Lock, set_cache_coro: Callable[["Graph", asyncio.Lock], Coroutine], vertex: Vertex
|
||||
):
|
||||
"""
|
||||
Retrieves the next runnable vertices and the top level vertices for a given vertex.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import asyncio
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Coroutine, List
|
||||
from typing import TYPE_CHECKING, Awaitable, Callable, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.graph.base import Graph
|
||||
|
|
@ -55,7 +55,7 @@ class RunnableVerticesManager:
|
|||
async def get_next_runnable_vertices(
|
||||
self,
|
||||
lock: asyncio.Lock,
|
||||
set_cache_coro: Coroutine,
|
||||
set_cache_coro: Callable[["Graph", asyncio.Lock], Awaitable[None]],
|
||||
graph: "Graph",
|
||||
vertex: "Vertex",
|
||||
):
|
||||
|
|
@ -85,7 +85,7 @@ class RunnableVerticesManager:
|
|||
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
|
||||
self.update_vertex_run_state(v_id, is_runnable=False)
|
||||
self.remove_from_predecessors(v_id)
|
||||
await set_cache_coro(data=graph, lock=lock)
|
||||
await set_cache_coro(graph, lock)
|
||||
return next_runnable_vertices
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import inspect
|
|||
import types
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from langflow.graph.schema import INPUT_COMPONENTS, OUTPUT_COMPONENTS, InterfaceComponentTypes, ResultData
|
||||
|
|
@ -68,6 +67,7 @@ class Vertex:
|
|||
self.is_task = is_task
|
||||
self.params = params or {}
|
||||
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
|
||||
self.load_from_db_fields: List[str] = []
|
||||
self.parent_is_top_level = False
|
||||
self.layer = None
|
||||
self.should_run = True
|
||||
|
|
@ -155,6 +155,7 @@ class Vertex:
|
|||
"_built": False,
|
||||
"parent_node_id": self.parent_node_id,
|
||||
"parent_is_top_level": self.parent_is_top_level,
|
||||
"load_from_db_fields": self.load_from_db_fields,
|
||||
"is_input": self.is_input,
|
||||
"is_output": self.is_output,
|
||||
}
|
||||
|
|
@ -185,6 +186,7 @@ class Vertex:
|
|||
self.task_id: Optional[str] = None
|
||||
self.parent_node_id = state["parent_node_id"]
|
||||
self.parent_is_top_level = state["parent_is_top_level"]
|
||||
self.load_from_db_fields = state["load_from_db_fields"]
|
||||
self.layer = state.get("layer")
|
||||
self.steps = state.get("steps", [self._build])
|
||||
|
||||
|
|
@ -285,20 +287,21 @@ class Vertex:
|
|||
else:
|
||||
params[param_key] = self.graph.get_vertex(edge.source_id)
|
||||
|
||||
for key, value in template_dict.items():
|
||||
if key in params:
|
||||
load_from_db_fields = []
|
||||
for field_name, field in template_dict.items():
|
||||
if field_name in params:
|
||||
continue
|
||||
# Skip _type and any value that has show == False and is not code
|
||||
# If we don't want to show code but we want to use it
|
||||
if key == "_type" or (not value.get("show") and key != "code"):
|
||||
if field_name == "_type" or (not field.get("show") and field_name != "code"):
|
||||
continue
|
||||
# If the type is not transformable to a python base class
|
||||
# then we need to get the edge that connects to this node
|
||||
if value.get("type") == "file":
|
||||
if field.get("type") == "file":
|
||||
# Load the type in value.get('fileTypes') using
|
||||
# what is inside value.get('content')
|
||||
# value.get('value') is the file name
|
||||
if file_path := value.get("file_path"):
|
||||
if file_path := field.get("file_path"):
|
||||
storage_service = get_storage_service()
|
||||
try:
|
||||
flow_id, file_name = file_path.split("/")
|
||||
|
|
@ -308,51 +311,58 @@ class Vertex:
|
|||
full_path = file_path
|
||||
else:
|
||||
raise e
|
||||
params[key] = full_path
|
||||
elif value.get("required"):
|
||||
params[field_name] = full_path
|
||||
elif field.get("required"):
|
||||
raise ValueError(f"File path not found for {self.display_name}")
|
||||
elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
|
||||
val = value.get("value")
|
||||
if value.get("type") == "code":
|
||||
elif field.get("type") in DIRECT_TYPES and params.get(field_name) is None:
|
||||
val = field.get("value")
|
||||
if field.get("type") == "code":
|
||||
try:
|
||||
params[key] = ast.literal_eval(val) if val else None
|
||||
params[field_name] = ast.literal_eval(val) if val else None
|
||||
except Exception:
|
||||
params[key] = val
|
||||
elif value.get("type") in ["dict", "NestedDict"]:
|
||||
params[field_name] = val
|
||||
elif field.get("type") in ["dict", "NestedDict"]:
|
||||
# When dict comes from the frontend it comes as a
|
||||
# list of dicts, so we need to convert it to a dict
|
||||
# before passing it to the build method
|
||||
if isinstance(val, list):
|
||||
params[key] = {k: v for item in value.get("value", []) for k, v in item.items()}
|
||||
params[field_name] = {k: v for item in field.get("value", []) for k, v in item.items()}
|
||||
elif isinstance(val, dict):
|
||||
params[key] = val
|
||||
elif value.get("type") == "int" and val is not None:
|
||||
params[field_name] = val
|
||||
elif field.get("type") == "int" and val is not None:
|
||||
try:
|
||||
params[key] = int(val)
|
||||
params[field_name] = int(val)
|
||||
except ValueError:
|
||||
params[key] = val
|
||||
elif value.get("type") == "float" and val is not None:
|
||||
params[field_name] = val
|
||||
elif field.get("type") == "float" and val is not None:
|
||||
try:
|
||||
params[key] = float(val)
|
||||
params[field_name] = float(val)
|
||||
except ValueError:
|
||||
params[key] = val
|
||||
elif value.get("type") == "str" and val is not None:
|
||||
params[field_name] = val
|
||||
params[field_name] = val
|
||||
elif field.get("type") == "str" and val is not None:
|
||||
# val may contain escaped \n, \t, etc.
|
||||
# so we need to unescape it
|
||||
if isinstance(val, list):
|
||||
params[key] = [unescape_string(v) for v in val]
|
||||
params[field_name] = [unescape_string(v) for v in val]
|
||||
elif isinstance(val, str):
|
||||
params[key] = unescape_string(val)
|
||||
params[field_name] = unescape_string(val)
|
||||
elif val is not None and val != "":
|
||||
params[key] = val
|
||||
params[field_name] = val
|
||||
|
||||
if not value.get("required") and params.get(key) is None:
|
||||
if value.get("default"):
|
||||
params[key] = value.get("default")
|
||||
elif val is not None and val != "":
|
||||
params[field_name] = val
|
||||
if field.get("load_from_db"):
|
||||
load_from_db_fields.append(field_name)
|
||||
|
||||
if not field.get("required") and params.get(field_name) is None:
|
||||
if field.get("default"):
|
||||
params[field_name] = field.get("default")
|
||||
else:
|
||||
params.pop(key, None)
|
||||
params.pop(field_name, None)
|
||||
# Add _type to params
|
||||
self.params = params
|
||||
self.load_from_db_fields = load_from_db_fields
|
||||
self._raw_params = params.copy()
|
||||
|
||||
def update_raw_params(self, new_params: Dict[str, str], overwrite: bool = False):
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from .record import docs_to_records, records_to_text
|
||||
|
||||
__all__ = ["docs_to_records", "records_to_text"]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple, Type, Union, cast
|
||||
|
||||
from pydantic.v1 import BaseModel, Field, create_model
|
||||
from sqlmodel import select
|
||||
|
|
@ -63,28 +63,30 @@ def find_flow(flow_name: str, user_id: str) -> Optional[str]:
|
|||
|
||||
|
||||
async def run_flow(
|
||||
inputs: Union[dict, List[dict]] = None,
|
||||
inputs: Optional[Union[dict, List[dict]]] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
flow_id: Optional[str] = None,
|
||||
flow_name: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> Any:
|
||||
if not user_id:
|
||||
raise ValueError("Session is invalid")
|
||||
graph = await load_flow(user_id, flow_id, flow_name, tweaks)
|
||||
|
||||
if inputs is None:
|
||||
inputs = []
|
||||
inputs_list = []
|
||||
inputs_list: list[dict[str, str]] = []
|
||||
inputs_components = []
|
||||
types = []
|
||||
for input_dict in inputs:
|
||||
inputs_list.append({INPUT_FIELD_NAME: input_dict.get("input_value")})
|
||||
inputs_list.append({INPUT_FIELD_NAME: cast(str, input_dict.get("input_value", ""))})
|
||||
inputs_components.append(input_dict.get("components", []))
|
||||
types.append(input_dict.get("type", []))
|
||||
|
||||
return await graph.arun(inputs_list, inputs_components=inputs_components, types=types)
|
||||
|
||||
|
||||
def generate_function_for_flow(inputs: List["Vertex"], flow_id: str) -> Coroutine:
|
||||
def generate_function_for_flow(inputs: List["Vertex"], flow_id: str) -> Callable[..., Awaitable[Any]]:
|
||||
"""
|
||||
Generate a dynamic flow function based on the given inputs and flow ID.
|
||||
|
||||
|
|
@ -138,12 +140,14 @@ async def flow_function({func_args}):
|
|||
"""
|
||||
|
||||
compiled_func = compile(func_body, "<string>", "exec")
|
||||
local_scope = {}
|
||||
local_scope: dict = {}
|
||||
exec(compiled_func, globals(), local_scope)
|
||||
return local_scope["flow_function"]
|
||||
|
||||
|
||||
def build_function_and_schema(flow_record: Record, graph: "Graph") -> Tuple[Callable, BaseModel]:
|
||||
def build_function_and_schema(
|
||||
flow_record: Record, graph: "Graph"
|
||||
) -> Tuple[Callable[..., Awaitable[Any]], Type[BaseModel]]:
|
||||
"""
|
||||
Builds a dynamic function and schema for a given flow.
|
||||
|
||||
|
|
@ -178,7 +182,7 @@ def get_flow_inputs(graph: "Graph") -> List["Vertex"]:
|
|||
return inputs
|
||||
|
||||
|
||||
def build_schema_from_inputs(name: str, inputs: List[tuple[str, str, str]]) -> BaseModel:
|
||||
def build_schema_from_inputs(name: str, inputs: List["Vertex"]) -> Type[BaseModel]:
|
||||
"""
|
||||
Builds a schema from the given inputs.
|
||||
|
||||
|
|
@ -196,4 +200,4 @@ def build_schema_from_inputs(name: str, inputs: List[tuple[str, str, str]]) -> B
|
|||
field_name = input_.display_name.lower().replace(" ", "_")
|
||||
description = input_.description
|
||||
fields[field_name] = (str, Field(default="", description=description))
|
||||
return create_model(name, **fields)
|
||||
return create_model(name, **fields) # type: ignore
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ class Component:
|
|||
def __setattr__(self, key, value):
|
||||
if key == "_user_id" and hasattr(self, "_user_id"):
|
||||
warnings.warn("user_id is immutable and cannot be changed.")
|
||||
else:
|
||||
super().__setattr__(key, value)
|
||||
super().__setattr__(key, value)
|
||||
|
||||
@cachedmethod(cache=operator.attrgetter("cache"))
|
||||
def get_code_tree(self, code: str):
|
||||
|
|
@ -70,6 +69,12 @@ class Component:
|
|||
return validate.create_function(self.code, self._function_entrypoint_name)
|
||||
|
||||
def build_template_config(self) -> dict:
|
||||
"""
|
||||
Builds the template configuration for the custom component.
|
||||
|
||||
Returns:
|
||||
A dictionary representing the template configuration.
|
||||
"""
|
||||
if not self.code:
|
||||
return {}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ from langflow.interface.custom.code_parser.utils import (
|
|||
extract_union_types_from_generic_alias,
|
||||
)
|
||||
from langflow.interface.custom.custom_component.component import Component
|
||||
from langflow.schema import dotdict
|
||||
from langflow.schema.schema import Record
|
||||
from langflow.services.deps import get_credential_service, get_storage_service, session_scope
|
||||
from langflow.schema import Record
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.services.deps import get_storage_service, get_variable_service, session_scope
|
||||
from langflow.services.storage.service import StorageService
|
||||
from langflow.utils import validate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -26,6 +27,23 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class CustomComponent(Component):
|
||||
"""
|
||||
Represents a custom component in Langflow.
|
||||
|
||||
Attributes:
|
||||
display_name (Optional[str]): The display name of the custom component.
|
||||
description (Optional[str]): The description of the custom component.
|
||||
code (Optional[str]): The code of the custom component.
|
||||
field_config (dict): The field configuration of the custom component.
|
||||
code_class_base_inheritance (ClassVar[str]): The base class name for the custom component.
|
||||
function_entrypoint_name (ClassVar[str]): The name of the function entrypoint for the custom component.
|
||||
function (Optional[Callable]): The function associated with the custom component.
|
||||
repr_value (Optional[Any]): The representation value of the custom component.
|
||||
user_id (Optional[Union[UUID, str]]): The user ID associated with the custom component.
|
||||
status (Optional[Any]): The status of the custom component.
|
||||
_tree (Optional[dict]): The code tree of the custom component.
|
||||
"""
|
||||
|
||||
display_name: Optional[str] = None
|
||||
"""The display name of the component. Defaults to None."""
|
||||
description: Optional[str] = None
|
||||
|
|
@ -88,6 +106,12 @@ class CustomComponent(Component):
|
|||
_tree: Optional[dict] = None
|
||||
|
||||
def __init__(self, **data):
|
||||
"""
|
||||
Initializes a new instance of the CustomComponent class.
|
||||
|
||||
Args:
|
||||
**data: Additional keyword arguments to initialize the custom component.
|
||||
"""
|
||||
self.cache = TTLCache(maxsize=1024, ttl=60)
|
||||
super().__init__(**data)
|
||||
|
||||
|
|
@ -115,6 +139,12 @@ class CustomComponent(Component):
|
|||
return self.field_order or list(self.field_config.keys())
|
||||
|
||||
def custom_repr(self):
|
||||
"""
|
||||
Returns the custom representation of the custom component.
|
||||
|
||||
Returns:
|
||||
str: The custom representation of the custom component.
|
||||
"""
|
||||
if self.repr_value == "":
|
||||
self.repr_value = self.status
|
||||
if isinstance(self.repr_value, dict):
|
||||
|
|
@ -126,6 +156,12 @@ class CustomComponent(Component):
|
|||
return self.repr_value
|
||||
|
||||
def build_config(self):
|
||||
"""
|
||||
Builds the configuration for the custom component.
|
||||
|
||||
Returns:
|
||||
dict: The configuration for the custom component.
|
||||
"""
|
||||
return self.field_config
|
||||
|
||||
def update_build_config(
|
||||
|
|
@ -139,6 +175,12 @@ class CustomComponent(Component):
|
|||
|
||||
@property
|
||||
def tree(self):
|
||||
"""
|
||||
Gets the code tree of the custom component.
|
||||
|
||||
Returns:
|
||||
dict: The code tree of the custom component.
|
||||
"""
|
||||
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]:
|
||||
|
|
@ -215,6 +257,12 @@ class CustomComponent(Component):
|
|||
|
||||
@property
|
||||
def get_function_entrypoint_args(self) -> list:
|
||||
"""
|
||||
Gets the arguments of the function entrypoint for the custom component.
|
||||
|
||||
Returns:
|
||||
list: The arguments of the function entrypoint.
|
||||
"""
|
||||
build_method = self.get_build_method()
|
||||
if not build_method:
|
||||
return []
|
||||
|
|
@ -228,6 +276,12 @@ class CustomComponent(Component):
|
|||
|
||||
@cachedmethod(operator.attrgetter("cache"))
|
||||
def get_build_method(self):
|
||||
"""
|
||||
Gets the build method for the custom component.
|
||||
|
||||
Returns:
|
||||
dict: The build method for the custom component.
|
||||
"""
|
||||
if not self.code:
|
||||
return {}
|
||||
|
||||
|
|
@ -245,6 +299,12 @@ class CustomComponent(Component):
|
|||
|
||||
@property
|
||||
def get_function_entrypoint_return_type(self) -> List[Any]:
|
||||
"""
|
||||
Gets the return type of the function entrypoint for the custom component.
|
||||
|
||||
Returns:
|
||||
List[Any]: The return type of the function entrypoint.
|
||||
"""
|
||||
build_method = self.get_build_method()
|
||||
if not build_method or not build_method.get("has_return"):
|
||||
return []
|
||||
|
|
@ -266,6 +326,12 @@ class CustomComponent(Component):
|
|||
|
||||
@property
|
||||
def get_main_class_name(self):
|
||||
"""
|
||||
Gets the main class name of the custom component.
|
||||
|
||||
Returns:
|
||||
str: The main class name of the custom component.
|
||||
"""
|
||||
if not self.code:
|
||||
return ""
|
||||
|
||||
|
|
@ -284,31 +350,63 @@ class CustomComponent(Component):
|
|||
|
||||
@property
|
||||
def template_config(self):
|
||||
"""
|
||||
Gets the template configuration for the custom component.
|
||||
|
||||
Returns:
|
||||
dict: The template configuration for the custom component.
|
||||
"""
|
||||
return self.build_template_config()
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
"""
|
||||
Returns the credential for the current user with the specified name.
|
||||
|
||||
Raises:
|
||||
ValueError: If the user id is not set.
|
||||
|
||||
Returns:
|
||||
The credential for the current user with the specified name.
|
||||
"""
|
||||
|
||||
def get_credential(name: str):
|
||||
if hasattr(self, "_user_id") and not self._user_id:
|
||||
raise ValueError(f"User id is not set for {self.__class__.__name__}")
|
||||
credential_service = get_credential_service() # Get service instance
|
||||
variable_service = get_variable_service() # Get service instance
|
||||
# Retrieve and decrypt the credential by name for the current user
|
||||
|
||||
with session_scope() as session:
|
||||
return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session)
|
||||
return variable_service.get_credential(user_id=self._user_id or "", name=name, session=session)
|
||||
|
||||
return get_credential
|
||||
|
||||
def list_key_names(self):
|
||||
"""
|
||||
Lists the names of the variables for the current user.
|
||||
|
||||
Raises:
|
||||
ValueError: If the user id is not set.
|
||||
|
||||
Returns:
|
||||
List[str]: The names of the variables for the current user.
|
||||
"""
|
||||
if hasattr(self, "_user_id") and not self._user_id:
|
||||
raise ValueError(f"User id is not set for {self.__class__.__name__}")
|
||||
credential_service = get_credential_service()
|
||||
variable_service = get_variable_service()
|
||||
|
||||
with session_scope() as session:
|
||||
return credential_service.list_credentials(user_id=self._user_id, session=session)
|
||||
return variable_service.list_variables(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."""
|
||||
"""
|
||||
Returns a function that returns the value at the given index in the iterable.
|
||||
|
||||
Args:
|
||||
value (int): The index value.
|
||||
|
||||
Returns:
|
||||
Callable: A function that returns the value at the given index.
|
||||
"""
|
||||
|
||||
def get_index(iterable: List[Any]):
|
||||
return iterable[value] if iterable else iterable
|
||||
|
|
@ -316,14 +414,22 @@ class CustomComponent(Component):
|
|||
return get_index
|
||||
|
||||
def get_function(self):
|
||||
"""
|
||||
Gets the function associated with the custom component.
|
||||
|
||||
Returns:
|
||||
Callable: The function associated with the custom component.
|
||||
"""
|
||||
return validate.create_function(self.code, self.function_entrypoint_name)
|
||||
|
||||
async def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> "Graph":
|
||||
return await load_flow(flow_id, tweaks)
|
||||
if not self._user_id:
|
||||
raise ValueError("Session is invalid")
|
||||
return await load_flow(user_id=self._user_id, flow_id=flow_id, tweaks=tweaks)
|
||||
|
||||
async def run_flow(
|
||||
self,
|
||||
inputs: Union[dict, List[dict]] = None,
|
||||
inputs: Optional[Union[dict, List[dict]]] = None,
|
||||
flow_id: Optional[str] = None,
|
||||
flow_name: Optional[str] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
|
|
@ -339,4 +445,14 @@ class CustomComponent(Component):
|
|||
raise ValueError(f"Error listing flows: {e}")
|
||||
|
||||
def build(self, *args: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Builds the custom component.
|
||||
|
||||
Args:
|
||||
*args: The positional arguments.
|
||||
**kwargs: The keyword arguments.
|
||||
|
||||
Returns:
|
||||
Any: The result of the build process.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import os
|
|||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class CustomComponentPathValueError(ValueError):
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ from langflow.utils import validate
|
|||
from langflow.utils.util import unescape_string
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
async def instantiate_class(
|
||||
|
|
@ -38,7 +38,7 @@ async def instantiate_class(
|
|||
user_id=None,
|
||||
) -> Any:
|
||||
"""Instantiate class from module type and key, and params"""
|
||||
from langflow.legacy_custom.customs import CUSTOM_NODES
|
||||
from langflow.interface.custom_lists import CUSTOM_NODES
|
||||
|
||||
vertex_type = vertex.vertex_type
|
||||
base_type = vertex.base_type
|
||||
|
|
@ -50,7 +50,9 @@ async def instantiate_class(
|
|||
if custom_node := CUSTOM_NODES.get(vertex_type):
|
||||
if hasattr(custom_node, "initialize"):
|
||||
return custom_node.initialize(**params)
|
||||
return custom_node(**params)
|
||||
if callable(custom_node):
|
||||
return custom_node(**params)
|
||||
raise ValueError(f"Custom node {vertex_type} is not callable")
|
||||
logger.debug(f"Instantiating {vertex_type} of type {base_type}")
|
||||
if not base_type:
|
||||
raise ValueError("No base type provided for vertex")
|
||||
|
|
@ -143,6 +145,21 @@ async def instantiate_based_on_type(
|
|||
return class_object(**params)
|
||||
|
||||
|
||||
def update_params_with_load_from_db_fields(custom_component: "CustomComponent", params, load_from_db_fields):
|
||||
# For each field in load_from_db_fields, we will check if it's in the params
|
||||
# and if it is, we will get the value from the custom_component.keys(name)
|
||||
# and update the params with the value
|
||||
for field in load_from_db_fields:
|
||||
if field in params:
|
||||
try:
|
||||
key = custom_component.keys(params[field])
|
||||
params[field] = key if key else params[field]
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to get value for {field} from custom component. Error: {exc}")
|
||||
pass
|
||||
return params
|
||||
|
||||
|
||||
async def instantiate_custom_component(params, user_id, vertex):
|
||||
params_copy = params.copy()
|
||||
class_object: Type["CustomComponent"] = eval_custom_component_code(params_copy.pop("code"))
|
||||
|
|
@ -152,6 +169,7 @@ async def instantiate_custom_component(params, user_id, vertex):
|
|||
vertex=vertex,
|
||||
selected_output_type=vertex.selected_output_type,
|
||||
)
|
||||
params_copy = update_params_with_load_from_db_fields(custom_component, params_copy, vertex.load_from_db_fields)
|
||||
|
||||
if "retriever" in params_copy and hasattr(params_copy["retriever"], "as_retriever"):
|
||||
params_copy["retriever"] = params_copy["retriever"].as_retriever()
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ from langflow.interface.base import LangChainTypeCreator
|
|||
from langflow.interface.tools.constants import ALL_TOOLS_NAMES, CUSTOM_TOOLS, FILE_TOOLS, OTHER_TOOLS
|
||||
from langflow.interface.tools.util import get_tool_params
|
||||
from langflow.legacy_custom import customs
|
||||
from langflow.interface.tools.util import get_tool_params
|
||||
from langflow.legacy_custom import customs
|
||||
from langflow.services.deps import get_settings_service
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.template.template.base import Template
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from langflow.template import frontend_node
|
||||
|
||||
# These should always be instantiated
|
||||
CUSTOM_NODES = {
|
||||
CUSTOM_NODES: dict[str, dict[str, frontend_node.base.FrontendNode]] = {
|
||||
# "prompts": {
|
||||
# "ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode(),
|
||||
# },
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class Result(BaseModel):
|
|||
|
||||
|
||||
async def run_graph(
|
||||
graph: Union["Graph", dict],
|
||||
graph: "Graph",
|
||||
flow_id: str,
|
||||
stream: bool,
|
||||
session_id: Optional[str] = None,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from .api_key import ApiKey
|
||||
from .credential import Credential
|
||||
from .flow import Flow
|
||||
from .user import User
|
||||
from .variable import Variable
|
||||
|
||||
__all__ = ["Flow", "User", "ApiKey", "Credential"]
|
||||
__all__ = ["Flow", "User", "ApiKey", "Variable"]
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from .model import Credential, CredentialCreate, CredentialRead, CredentialUpdate
|
||||
|
||||
__all__ = ["Credential", "CredentialCreate", "CredentialRead", "CredentialUpdate"]
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
from langflow.services.database.models.credential.schema import CredentialType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.user import User
|
||||
|
||||
|
||||
class CredentialBase(SQLModel):
|
||||
name: Optional[str] = Field(None, description="Name of the credential")
|
||||
value: Optional[str] = Field(None, description="Encrypted value of the credential")
|
||||
provider: Optional[str] = Field(None, description="Provider of the credential (e.g OpenAI)")
|
||||
|
||||
|
||||
class Credential(CredentialBase, table=True):
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True, description="Unique ID for the credential")
|
||||
# name is unique per user
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time of the credential")
|
||||
updated_at: Optional[datetime] = Field(None, description="Last update time of the credential")
|
||||
# foreign key to user table
|
||||
user_id: UUID = Field(description="User ID associated with this credential", foreign_key="user.id")
|
||||
user: "User" = Relationship(back_populates="credentials")
|
||||
|
||||
|
||||
class CredentialCreate(CredentialBase):
|
||||
# AcceptedProviders is a custom Enum
|
||||
provider: CredentialType = Field(description="Provider of the credential (e.g OpenAI)")
|
||||
|
||||
|
||||
class CredentialRead(SQLModel):
|
||||
id: UUID
|
||||
name: Optional[str] = Field(None, description="Name of the credential")
|
||||
provider: Optional[str] = Field(None, description="Provider of the credential (e.g OpenAI)")
|
||||
|
||||
|
||||
class CredentialUpdate(SQLModel):
|
||||
id: UUID # Include the ID for updating
|
||||
name: Optional[str] = Field(None, description="Name of the credential")
|
||||
value: Optional[str] = Field(None, description="Encrypted value of the credential")
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class CredentialType(str, Enum):
|
||||
"""CredentialType is an Enum of the accepted providers"""
|
||||
|
||||
OPENAI_API_KEY = "OPENAI_API_KEY"
|
||||
ANTHROPIC_API_KEY = "ANTHROPIC_API_KEY"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Path: src/backend/langflow/database/models/flow.py
|
||||
# Path: src/backend/langflow/services/database/models/flow/model.py
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from sqlmodel import Field, Relationship, SQLModel
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.api_key import ApiKey
|
||||
from langflow.services.database.models.credential import Credential
|
||||
from langflow.services.database.models.variable import Variable
|
||||
from langflow.services.database.models.flow import Flow
|
||||
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ class User(SQLModel, table=True):
|
|||
)
|
||||
store_api_key: Optional[str] = Field(default=None, nullable=True)
|
||||
flows: list["Flow"] = Relationship(back_populates="user")
|
||||
credentials: list["Credential"] = Relationship(
|
||||
variables: list["Variable"] = Relationship(
|
||||
back_populates="user",
|
||||
sa_relationship_kwargs={"cascade": "delete"},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from .model import Variable, VariableCreate, VariableRead, VariableUpdate
|
||||
|
||||
__all__ = ["Variable", "VariableCreate", "VariableRead", "VariableUpdate"]
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
from datetime import datetime,timezone
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
|
||||
def utc_now():
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class VariableBase(SQLModel):
|
||||
name: Optional[str] = Field(None, description="Name of the variable")
|
||||
value: Optional[str] = Field(None, description="Encrypted value of the variable")
|
||||
type: Optional[str] = Field(None, description="Type of the variable")
|
||||
|
||||
|
||||
class Variable(VariableBase, table=True):
|
||||
id: Optional[UUID] = Field(
|
||||
default_factory=uuid4,
|
||||
primary_key=True,
|
||||
description="Unique ID for the variable",
|
||||
)
|
||||
# name is unique per user
|
||||
created_at: datetime = Field(
|
||||
default_factory=utc_now, description="Creation time of the variable"
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
None, description="Last update time of the variable"
|
||||
)
|
||||
# foreign key to user table
|
||||
user_id: UUID = Field(
|
||||
description="User ID associated with this variable", foreign_key="user.id"
|
||||
)
|
||||
user: "User" = Relationship(back_populates="variables")
|
||||
|
||||
|
||||
class VariableCreate(VariableBase):
|
||||
type: Optional[str] = Field(None, description="Type of the variable")
|
||||
|
||||
|
||||
class VariableRead(SQLModel):
|
||||
id: UUID
|
||||
name: Optional[str] = Field(None, description="Name of the variable")
|
||||
type: Optional[str] = Field(None, description="Type of the variable")
|
||||
|
||||
|
||||
class VariableUpdate(SQLModel):
|
||||
id: UUID # Include the ID for updating
|
||||
name: Optional[str] = Field(None, description="Name of the variable")
|
||||
value: Optional[str] = Field(None, description="Encrypted value of the variable")
|
||||
|
|
@ -8,7 +8,6 @@ if TYPE_CHECKING:
|
|||
|
||||
from langflow.services.cache.service import CacheService
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.credentials.service import CredentialService
|
||||
from langflow.services.database.service import DatabaseService
|
||||
from langflow.services.monitor.service import MonitorService
|
||||
from langflow.services.plugins.service import PluginService
|
||||
|
|
@ -18,6 +17,7 @@ if TYPE_CHECKING:
|
|||
from langflow.services.storage.service import StorageService
|
||||
from langflow.services.store.service import StoreService
|
||||
from langflow.services.task.service import TaskService
|
||||
from langflow.services.variable.service import VariableService
|
||||
|
||||
|
||||
def get_socket_service() -> "SocketIOService":
|
||||
|
|
@ -28,8 +28,8 @@ def get_storage_service() -> "StorageService":
|
|||
return service_manager.get(ServiceType.STORAGE_SERVICE) # type: ignore
|
||||
|
||||
|
||||
def get_credential_service() -> "CredentialService":
|
||||
return service_manager.get(ServiceType.CREDENTIAL_SERVICE) # type: ignore
|
||||
def get_variable_service() -> "VariableService":
|
||||
return service_manager.get(ServiceType.VARIABLE_SERVICE) # type: ignore
|
||||
|
||||
|
||||
def get_plugins_service() -> "PluginService":
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class ServiceFactory:
|
|||
raise self.service_class(*args, **kwargs)
|
||||
|
||||
|
||||
def hash_factory(factory: ServiceFactory) -> str:
|
||||
def hash_factory(factory: Type[ServiceFactory]) -> str:
|
||||
return factory.service_class.__name__
|
||||
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ def hash_infer_service_types_args(factory_class: Type[ServiceFactory], available
|
|||
|
||||
|
||||
@cached(cache=LRUCache(maxsize=10), key=hash_infer_service_types_args)
|
||||
def infer_service_types(factory_class: Type[ServiceFactory], available_services=None) -> "ServiceType":
|
||||
def infer_service_types(factory_class: Type[ServiceFactory], available_services=None) -> list["ServiceType"]:
|
||||
create_method = factory_class.create
|
||||
type_hints = get_type_hints(create_method, globalns=available_services)
|
||||
service_types = []
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ServiceType(str, Enum):
|
|||
TASK_SERVICE = "task_service"
|
||||
PLUGINS_SERVICE = "plugins_service"
|
||||
STORE_SERVICE = "store_service"
|
||||
CREDENTIALS_SERVICE = "credentials_service"
|
||||
VARIABLE_SERVICE = "variable_service"
|
||||
STORAGE_SERVICE = "storage_service"
|
||||
MONITOR_SERVICE = "monitor_service"
|
||||
SOCKETIO_SERVICE = "socket_service"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from loguru import logger
|
||||
from passlib.context import CryptContext
|
||||
|
|
@ -14,7 +15,7 @@ class AuthSettings(BaseSettings):
|
|||
# Login settings
|
||||
CONFIG_DIR: str
|
||||
SECRET_KEY: SecretStr = Field(
|
||||
default=None,
|
||||
default=SecretStr(""),
|
||||
description="Secret key for JWT. If not provided, a random one will be generated.",
|
||||
frozen=False,
|
||||
)
|
||||
|
|
@ -33,13 +34,13 @@ class AuthSettings(BaseSettings):
|
|||
SUPERUSER: str = DEFAULT_SUPERUSER
|
||||
SUPERUSER_PASSWORD: str = DEFAULT_SUPERUSER_PASSWORD
|
||||
|
||||
REFRESH_SAME_SITE: str = "none"
|
||||
REFRESH_SAME_SITE: Literal["lax", "strict", "none"] = "none"
|
||||
"""The SameSite attribute of the refresh token cookie."""
|
||||
REFRESH_SECURE: bool = True
|
||||
"""The Secure attribute of the refresh token cookie."""
|
||||
REFRESH_HTTPONLY: bool = True
|
||||
"""The HttpOnly attribute of the refresh token cookie."""
|
||||
ACCESS_SAME_SITE: str = "none"
|
||||
ACCESS_SAME_SITE: Literal["lax", "strict", "none"] = "none"
|
||||
"""The SameSite attribute of the access token cookie."""
|
||||
ACCESS_SECURE: bool = True
|
||||
"""The Secure attribute of the access token cookie."""
|
||||
|
|
@ -85,9 +86,10 @@ class AuthSettings(BaseSettings):
|
|||
|
||||
secret_key_path = Path(config_dir) / "secret_key"
|
||||
|
||||
if value:
|
||||
if value and isinstance(value, SecretStr):
|
||||
logger.debug("Secret key provided")
|
||||
write_secret_to_file(secret_key_path, value)
|
||||
secret_value = value.get_secret_value()
|
||||
write_secret_to_file(secret_key_path, secret_value)
|
||||
else:
|
||||
logger.debug("No secret key provided, generating a random one")
|
||||
|
||||
|
|
@ -103,4 +105,4 @@ class AuthSettings(BaseSettings):
|
|||
write_secret_to_file(secret_key_path, value)
|
||||
logger.debug("Saved secret key")
|
||||
|
||||
return value
|
||||
return value if isinstance(value, SecretStr) else SecretStr(value)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from langflow.services.credentials.service import CredentialService
|
||||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.services.variable.service import VariableService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.service import SettingsService
|
||||
|
||||
|
||||
class CredentialServiceFactory(ServiceFactory):
|
||||
class VariableServiceFactory(ServiceFactory):
|
||||
def __init__(self):
|
||||
super().__init__(CredentialService)
|
||||
super().__init__(VariableService)
|
||||
|
||||
def create(self, settings_service: "SettingsService"):
|
||||
return CredentialService(settings_service)
|
||||
return VariableService(settings_service)
|
||||
|
|
@ -6,25 +6,23 @@ from sqlmodel import Session, select
|
|||
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
from langflow.services.base import Service
|
||||
from langflow.services.database.models.credential.model import Credential
|
||||
from langflow.services.database.models.variable.model import Variable
|
||||
from langflow.services.deps import get_session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.service import SettingsService
|
||||
|
||||
|
||||
class CredentialService(Service):
|
||||
name = "credential_service"
|
||||
class VariableService(Service):
|
||||
name = "variable_service"
|
||||
|
||||
def __init__(self, settings_service: "SettingsService"):
|
||||
self.settings_service = settings_service
|
||||
|
||||
def get_credential(self, user_id: Union[UUID, str], name: str, session: Session = Depends(get_session)) -> str:
|
||||
# we get the credential from the database
|
||||
# credential = session.query(Credential).filter(Credential.user_id == user_id, Credential.name == name).first()
|
||||
credential = session.exec(
|
||||
select(Credential).where(Credential.user_id == user_id, Credential.name == name)
|
||||
).first()
|
||||
# credential = session.query(Variable).filter(Variable.user_id == user_id, Variable.name == name).first()
|
||||
credential = session.exec(select(Variable).where(Variable.user_id == user_id, Variable.name == name)).first()
|
||||
# we decrypt the value
|
||||
if not credential or not credential.value:
|
||||
raise ValueError(f"{name} credential not found.")
|
||||
|
|
@ -34,5 +32,5 @@ class CredentialService(Service):
|
|||
def list_credentials(
|
||||
self, user_id: Union[UUID, str], session: Session = Depends(get_session)
|
||||
) -> list[Optional[str]]:
|
||||
credentials = session.exec(select(Credential).where(Credential.user_id == user_id)).all()
|
||||
credentials = session.exec(select(Variable).where(Variable.user_id == user_id)).all()
|
||||
return [credential.name for credential in credentials]
|
||||
|
|
@ -7,6 +7,7 @@ from langflow.field_typing.range_spec import RangeSpec
|
|||
|
||||
class TemplateField(BaseModel):
|
||||
model_config = ConfigDict()
|
||||
|
||||
field_type: str = Field(default="str", serialization_alias="type")
|
||||
"""The type of field this is. Default is a string."""
|
||||
|
||||
|
|
@ -69,6 +70,8 @@ class TemplateField(BaseModel):
|
|||
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
|
||||
"""Range specification for the field. Defaults to None."""
|
||||
|
||||
load_from_db: bool = False
|
||||
"""Specifies if the field should be loaded from the database. Defaults to False."""
|
||||
title_case: bool = False
|
||||
"""Specifies if the field should be displayed in title case. Defaults to True."""
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ from langflow.template.frontend_node import (
|
|||
textsplitters,
|
||||
tools,
|
||||
vectorstores,
|
||||
base,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"agents",
|
||||
"base",
|
||||
"chains",
|
||||
"embeddings",
|
||||
"memories",
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ repository = "https://github.com/logspace-ai/langflow"
|
|||
license = "MIT"
|
||||
readme = "README.md"
|
||||
keywords = ["nlp", "langchain", "openai", "gpt", "gui"]
|
||||
packages = [{ include = "langflow" }]
|
||||
packages = [{ include = "langflow" }, { include = "langflow/py.typed" }]
|
||||
include = ["pyproject.toml", "README.md", "langflow/**/*"]
|
||||
documentation = "https://docs.langflow.org"
|
||||
|
||||
|
||||
[tool.poetry.scripts]
|
||||
langflow-base = "langflow.__main__:main"
|
||||
|
||||
|
|
@ -106,6 +107,11 @@ filterwarnings = ["ignore::DeprecationWarning"]
|
|||
log_cli = true
|
||||
markers = ["async_test"]
|
||||
|
||||
[tool.mypy]
|
||||
namespace_packages = true
|
||||
mypy_path = "langflow"
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
exclude = ["src/backend/langflow/alembic/*"]
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
from typing import List, Union
|
||||
|
||||
from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseMemory, Text, Tool
|
||||
|
||||
|
||||
class LCAgentComponent(CustomComponent):
|
||||
def build_config(self):
|
||||
return {
|
||||
"lc": {
|
||||
"display_name": "LangChain",
|
||||
"info": "The LangChain to interact with.",
|
||||
},
|
||||
"handle_parsing_errors": {
|
||||
"display_name": "Handle Parsing Errors",
|
||||
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
|
||||
"advanced": True,
|
||||
},
|
||||
"output_key": {
|
||||
"display_name": "Output Key",
|
||||
"info": "The key to use to get the output from the agent.",
|
||||
"advanced": True,
|
||||
},
|
||||
"memory": {
|
||||
"display_name": "Memory",
|
||||
"info": "Memory to use for the agent.",
|
||||
},
|
||||
"tools": {
|
||||
"display_name": "Tools",
|
||||
"info": "Tools the agent can use.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Input",
|
||||
"info": "Input text to pass to the agent.",
|
||||
},
|
||||
}
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
agent: Union[BaseSingleActionAgent, BaseMultiActionAgent, AgentExecutor],
|
||||
inputs: str,
|
||||
input_variables: list[str],
|
||||
tools: List[Tool],
|
||||
memory: BaseMemory = None,
|
||||
handle_parsing_errors: bool = True,
|
||||
output_key: str = "output",
|
||||
) -> Text:
|
||||
if isinstance(agent, AgentExecutor):
|
||||
runnable = agent
|
||||
else:
|
||||
runnable = AgentExecutor.from_agent_and_tools(
|
||||
agent=agent, tools=tools, verbose=True, memory=memory, handle_parsing_errors=handle_parsing_errors
|
||||
)
|
||||
input_dict = {"input": inputs}
|
||||
for var in input_variables:
|
||||
if var not in ["agent_scratchpad", "input"]:
|
||||
input_dict[var] = ""
|
||||
result = await runnable.ainvoke(input_dict)
|
||||
self.status = result
|
||||
if output_key in result:
|
||||
return result.get(output_key)
|
||||
elif "output" not in result:
|
||||
if output_key != "output":
|
||||
raise ValueError(f"Output key not found in result. Tried '{output_key}' and 'output'.")
|
||||
else:
|
||||
raise ValueError("Output key not found in result. Tried 'output'.")
|
||||
|
||||
return result.get("output")
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from .model import LCModelComponent
|
||||
|
||||
__all__ = ["LCModelComponent"]
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain_core.language_models.chat_models import BaseChatModel
|
||||
from langchain_core.language_models.llms import LLM
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class LCModelComponent(CustomComponent):
|
||||
display_name: str = "Model Name"
|
||||
description: str = "Model Description"
|
||||
|
||||
def get_result(self, runnable: LLM, stream: bool, input_value: str):
|
||||
"""
|
||||
Retrieves the result from the output of a Runnable object.
|
||||
|
||||
Args:
|
||||
output (Runnable): The output object to retrieve the result from.
|
||||
stream (bool): Indicates whether to use streaming or invocation mode.
|
||||
input_value (str): The input value to pass to the output object.
|
||||
|
||||
Returns:
|
||||
The result obtained from the output object.
|
||||
"""
|
||||
if stream:
|
||||
result = runnable.stream(input_value)
|
||||
else:
|
||||
message = runnable.invoke(input_value)
|
||||
result = message.content if hasattr(message, "content") else message
|
||||
self.status = result
|
||||
return result
|
||||
|
||||
def get_chat_result(
|
||||
self, runnable: BaseChatModel, stream: bool, input_value: str, system_message: Optional[str] = None
|
||||
):
|
||||
messages = []
|
||||
if input_value:
|
||||
messages.append(HumanMessage(input_value))
|
||||
if system_message:
|
||||
messages.append(SystemMessage(system_message))
|
||||
if stream:
|
||||
result = runnable.stream(messages)
|
||||
else:
|
||||
message = runnable.invoke(messages)
|
||||
result = message.content
|
||||
self.status = result
|
||||
return result
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
from typing import Any, List, Optional, Text
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
from loguru import logger
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Tool
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.helpers.flow import build_function_and_schema
|
||||
from langflow.schema.dotdict import dotdict
|
||||
|
||||
|
||||
class FlowToolComponent(CustomComponent):
|
||||
display_name = "Flow as Tool"
|
||||
description = "Construct a Tool from a function that runs the loaded Flow."
|
||||
field_order = ["flow_name", "name", "description", "return_direct"]
|
||||
|
||||
def get_flow_names(self) -> List[str]:
|
||||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def get_flow(self, flow_name: str) -> Optional[Text]:
|
||||
"""
|
||||
Retrieves a flow by its name.
|
||||
|
||||
Args:
|
||||
flow_name (str): The name of the flow to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[Text]: The flow record if found, None otherwise.
|
||||
"""
|
||||
flow_records = self.list_flows()
|
||||
for flow_record in flow_records:
|
||||
if flow_record.data["name"] == flow_name:
|
||||
return flow_record
|
||||
return None
|
||||
|
||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
|
||||
logger.debug(f"Updating build config with field value {field_value} and field name {field_name}")
|
||||
if field_name == "flow_name":
|
||||
build_config["flow_name"]["options"] = self.get_flow_names()
|
||||
|
||||
return build_config
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"flow_name": {
|
||||
"display_name": "Flow Name",
|
||||
"info": "The name of the flow to run.",
|
||||
"options": [],
|
||||
"real_time_refresh": True,
|
||||
"refresh_button": True,
|
||||
},
|
||||
"name": {
|
||||
"display_name": "Name",
|
||||
"description": "The name of the tool.",
|
||||
},
|
||||
"description": {
|
||||
"display_name": "Description",
|
||||
"description": "The description of the tool.",
|
||||
},
|
||||
"return_direct": {
|
||||
"display_name": "Return Direct",
|
||||
"description": "Return the result directly from the Tool.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
async def build(self, flow_name: str, name: str, description: str, return_direct: bool = False) -> Tool:
|
||||
flow_record = self.get_flow(flow_name)
|
||||
if not flow_record:
|
||||
raise ValueError("Flow not found.")
|
||||
graph = Graph.from_payload(flow_record.data["data"])
|
||||
dynamic_flow_function, schema = build_function_and_schema(flow_record, graph)
|
||||
tool = StructuredTool.from_function(
|
||||
coroutine=dynamic_flow_function,
|
||||
name=name,
|
||||
description=description,
|
||||
return_direct=return_direct,
|
||||
args_schema=schema,
|
||||
)
|
||||
description_repr = repr(tool.description).strip("'")
|
||||
args_str = "\n".join([f"- {arg_name}: {arg_data['description']}" for arg_name, arg_data in tool.args.items()])
|
||||
self.status = f"{description_repr}\nArguments:\n{args_str}"
|
||||
return tool
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class SchemaComponent(CustomComponent):
|
||||
display_name = "Schema"
|
||||
description = "Construct a Schema from a list of fields."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"fields": {
|
||||
"display_name": "Fields",
|
||||
"info": "The fields to include in the schema.",
|
||||
},
|
||||
"name": {
|
||||
"display_name": "Name",
|
||||
"info": "The name of the schema.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, name: str, fields: list[dict]):
|
||||
# The idea for this component is to use create_model from pydantic to create a schema
|
||||
# from a list of fields. This will be useful for creating schemas for the flow tool.
|
||||
pass
|
||||
|
||||
# field is a simple list of dictionaries with the field name and
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
from langchain_community.tools.searchapi import SearchAPIRun
|
||||
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Tool
|
||||
|
||||
|
||||
class SearchApiToolComponent(CustomComponent):
|
||||
display_name: str = "SearchApi Tool"
|
||||
description: str = "Real-time search engine results API."
|
||||
documentation: str = "https://www.searchapi.io/docs/google"
|
||||
field_config = {
|
||||
"engine": {
|
||||
"display_name": "Engine",
|
||||
"field_type": "str",
|
||||
"info": "The search engine to use.",
|
||||
},
|
||||
"api_key": {
|
||||
"display_name": "API Key",
|
||||
"field_type": "str",
|
||||
"required": True,
|
||||
"password": True,
|
||||
"info": "The API key to use SearchApi.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
engine: str,
|
||||
api_key: str,
|
||||
) -> Tool:
|
||||
search_api_wrapper = SearchApiAPIWrapper(engine=engine, searchapi_api_key=api_key)
|
||||
|
||||
tool = SearchAPIRun(api_wrapper=search_api_wrapper)
|
||||
|
||||
self.status = tool
|
||||
return tool
|
||||
1
src/backend/langflow/version/__init__.py
Normal file
1
src/backend/langflow/version/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .version import __version__ # noqa: F401
|
||||
|
|
@ -3,6 +3,5 @@ from importlib import metadata
|
|||
try:
|
||||
__version__ = metadata.version(__package__)
|
||||
except metadata.PackageNotFoundError:
|
||||
# Case where package metadata is not available.
|
||||
__version__ = ""
|
||||
del metadata # optional, avoids polluting the results of dir(__package__)
|
||||
del metadata
|
||||
787
src/frontend/package-lock.json
generated
787
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -37,6 +37,7 @@
|
|||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dompurify": "^3.0.5",
|
||||
"esbuild": "^0.17.19",
|
||||
"framer-motion": "^11.0.6",
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ import {
|
|||
FETCH_ERROR_MESSAGE,
|
||||
} from "./constants/constants";
|
||||
import { AuthContext } from "./contexts/authContext";
|
||||
import { getHealth } from "./controllers/API";
|
||||
import { getGlobalVariables, getHealth } from "./controllers/API";
|
||||
import Router from "./routes";
|
||||
import useAlertStore from "./stores/alertStore";
|
||||
import { useDarkStore } from "./stores/darkStore";
|
||||
import useFlowsManagerStore from "./stores/flowsManagerStore";
|
||||
import { useGlobalVariablesStore } from "./stores/globalVariables";
|
||||
import { useStoreStore } from "./stores/storeStore";
|
||||
import { useTypesStore } from "./stores/typesStore";
|
||||
|
||||
|
|
@ -43,6 +44,9 @@ export default function App() {
|
|||
const getTypes = useTypesStore((state) => state.getTypes);
|
||||
const refreshVersion = useDarkStore((state) => state.refreshVersion);
|
||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||
const setGlobalVariables = useGlobalVariablesStore(
|
||||
(state) => state.setGlobalVariables
|
||||
);
|
||||
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -58,6 +62,9 @@ export default function App() {
|
|||
getTypes().then(() => {
|
||||
refreshFlows();
|
||||
});
|
||||
getGlobalVariables().then((res) => {
|
||||
setGlobalVariables(res);
|
||||
});
|
||||
checkHasStore();
|
||||
fetchApiData();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
|||
import DictComponent from "../../../../components/dictComponent";
|
||||
import Dropdown from "../../../../components/dropdownComponent";
|
||||
import FloatComponent from "../../../../components/floatComponent";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import InputComponent from "../../../../components/inputComponent";
|
||||
import { default as IconComponent } from "../../../../components/genericIconComponent";
|
||||
import InputFileComponent from "../../../../components/inputFileComponent";
|
||||
import InputGlobalComponent from "../../../../components/inputGlobalComponent";
|
||||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import IntComponent from "../../../../components/intComponent";
|
||||
import KeypairListComponent from "../../../../components/keypairListComponent";
|
||||
|
|
@ -71,6 +71,7 @@ export default function ParameterComponent({
|
|||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const flow = currentFlow?.data?.nodes ?? null;
|
||||
|
||||
|
|
@ -381,9 +382,8 @@ export default function ParameterComponent({
|
|||
<>
|
||||
<div
|
||||
className={
|
||||
"w-full truncate text-sm" +
|
||||
(left ? "" : " flex items-center justify-end gap-2") +
|
||||
(info !== "" ? " flex items-center" : "")
|
||||
"flex w-full items-center truncate text-sm" +
|
||||
(left ? "" : " text-end")
|
||||
}
|
||||
>
|
||||
{!left && data.node?.frozen && (
|
||||
|
|
@ -536,12 +536,21 @@ export default function ParameterComponent({
|
|||
(data.node?.template[name].refresh_button ? "w-5/6" : "")
|
||||
}
|
||||
>
|
||||
<InputComponent
|
||||
id={"input-" + name}
|
||||
<InputGlobalComponent
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
setDb={(value) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template[name].load_from_db = value;
|
||||
return newNode;
|
||||
});
|
||||
}}
|
||||
name={name}
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import { useState } from "react";
|
||||
import { registerGlobalVariable } from "../../controllers/API";
|
||||
import BaseModal from "../../modals/baseModal";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariables";
|
||||
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import InputComponent from "../inputComponent";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
//TODO IMPLEMENT FORM LOGIC
|
||||
|
||||
export default function AddNewVariableButton({ children }): JSX.Element {
|
||||
const [key, setKey] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const addGlobalVariable = useGlobalVariablesStore(
|
||||
(state) => state.addGlobalVariable
|
||||
);
|
||||
function handleSaveVariable() {
|
||||
let data: { name: string; value: string; type?: string } = {
|
||||
name: key,
|
||||
type,
|
||||
value,
|
||||
};
|
||||
registerGlobalVariable(data)
|
||||
.then((res) => {
|
||||
const { name, id, type } = res.data;
|
||||
addGlobalVariable(name, id, type);
|
||||
setKey("");
|
||||
setValue("");
|
||||
setType("");
|
||||
setOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
setErrorData({
|
||||
title: "Error creating variable",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
});
|
||||
});
|
||||
}
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen} size="x-small">
|
||||
<BaseModal.Header
|
||||
description={
|
||||
"This variable will be encrypted and will be available for you to use in any of your projects."
|
||||
}
|
||||
>
|
||||
<span className="pr-2"> Create Variable </span>
|
||||
<ForwardedIconComponent
|
||||
name="Globe"
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Content>
|
||||
<div className="flex h-full w-full flex-col gap-4 align-middle">
|
||||
<Label>Variable Name</Label>
|
||||
<Input
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
setKey(e.target.value);
|
||||
}}
|
||||
placeholder="Insert a name for the variable..."
|
||||
></Input>
|
||||
<Label>Type (optional)</Label>
|
||||
<InputComponent
|
||||
setSelectedOption={(e) => {
|
||||
setType(e);
|
||||
}}
|
||||
selectedOption={type}
|
||||
password={false}
|
||||
options={["Variable", "Credential"]}
|
||||
placeholder="Choose a type for the variable..."
|
||||
></InputComponent>
|
||||
<Label>Value</Label>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder="Insert a value for the variable..."
|
||||
className="w-full resize-none custom-scroll"
|
||||
/>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
<Button onClick={handleSaveVariable}>Save variable</Button>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import AccordionComponent from "../../components/AccordionComponent";
|
|||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
import FloatComponent from "../../components/floatComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import InputFileComponent from "../../components/inputFileComponent";
|
||||
import InputListComponent from "../../components/inputListComponent";
|
||||
import IntComponent from "../../components/intComponent";
|
||||
|
|
@ -39,6 +38,7 @@ import {
|
|||
import { classNames } from "../../utils/utils";
|
||||
import DictComponent from "../dictComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import InputGlobalComponent from "../inputGlobalComponent";
|
||||
import KeypairListComponent from "../keypairListComponent";
|
||||
|
||||
export default function CodeTabsComponent({
|
||||
|
|
@ -54,6 +54,7 @@ export default function CodeTabsComponent({
|
|||
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const unselectAll = useFlowStore((state) => state.unselectAll);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
|
|
@ -344,38 +345,31 @@ export default function CodeTabsComponent({
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<InputComponent
|
||||
<InputGlobalComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
password={
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].password ?? false
|
||||
}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].value ||
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].value === ""
|
||||
? ""
|
||||
: node.data.node
|
||||
.template[
|
||||
templateField
|
||||
].value
|
||||
}
|
||||
onChange={(target) => {
|
||||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList![
|
||||
i
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
if (node.data) {
|
||||
setNode(
|
||||
node.data.id,
|
||||
(oldNode) => {
|
||||
let newNode =
|
||||
cloneDeep(
|
||||
oldNode
|
||||
);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
|
||||
newNode.data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
);
|
||||
}
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
|
|
@ -384,6 +378,25 @@ export default function CodeTabsComponent({
|
|||
]
|
||||
);
|
||||
}}
|
||||
setDb={(value) => {
|
||||
setNode(
|
||||
node.data.id,
|
||||
(oldNode) => {
|
||||
let newNode =
|
||||
cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template[
|
||||
templateField
|
||||
].load_from_db =
|
||||
value;
|
||||
return newNode;
|
||||
}
|
||||
);
|
||||
}}
|
||||
name={templateField}
|
||||
data={node.data}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function Dropdown({
|
|||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className={"relative mt-1"}>
|
||||
<div className={"relative"}>
|
||||
<Listbox.Button
|
||||
data-test={`${id ?? ""}`}
|
||||
className={
|
||||
|
|
@ -52,7 +52,7 @@ export default function Dropdown({
|
|||
>
|
||||
{internalValue}
|
||||
</span>
|
||||
<span className={"dropdown-component-arrow"}>
|
||||
<span className={"dropdown-component-arrow right-0"}>
|
||||
<IconComponent
|
||||
name="ChevronsUpDown"
|
||||
className="dropdown-component-arrow-color"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { InputComponentType } from "../../types/components";
|
||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "../ui/command";
|
||||
import { Input } from "../ui/input";
|
||||
import { Popover, PopoverContentWithoutPortal } from "../ui/popover";
|
||||
|
||||
export default function InputComponent({
|
||||
autoFocus = false,
|
||||
onBlur,
|
||||
value,
|
||||
value = "",
|
||||
onChange,
|
||||
disabled,
|
||||
required = false,
|
||||
|
|
@ -20,17 +30,30 @@ export default function InputComponent({
|
|||
className,
|
||||
id = "",
|
||||
blurOnEnter = false,
|
||||
optionsIcon = "ChevronsUpDown",
|
||||
selectedOption,
|
||||
setSelectedOption,
|
||||
options = [],
|
||||
optionsPlaceholder = "Search options...",
|
||||
optionsButton,
|
||||
optionButton,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled && value !== "") {
|
||||
if (disabled && value && onChange && value !== "") {
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
function onInputLostFocus(event): void {
|
||||
if (onBlur) onBlur(event);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
{isForm ? (
|
||||
|
|
@ -38,7 +61,7 @@ export default function InputComponent({
|
|||
<Input
|
||||
id={"form-" + id}
|
||||
ref={refInput}
|
||||
onBlur={onBlur}
|
||||
onBlur={onInputLostFocus}
|
||||
autoFocus={autoFocus}
|
||||
type={password && !pwdVisible ? "password" : "text"}
|
||||
value={value}
|
||||
|
|
@ -55,7 +78,7 @@ export default function InputComponent({
|
|||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
onChange && onChange(e.target.value);
|
||||
}}
|
||||
onCopy={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -67,54 +90,167 @@ export default function InputComponent({
|
|||
/>
|
||||
</Form.Control>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={classNames(
|
||||
password && !pwdVisible && value !== ""
|
||||
? " text-clip password "
|
||||
: "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && editNode ? "pr-8" : "",
|
||||
password && !editNode ? "pr-10" : "",
|
||||
className!
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
// if the user copies a password from another input
|
||||
// it might come as ••••••••••• it causes errors
|
||||
// in ascii encoding, so we need to handle it
|
||||
if (password && e.target.value.length > 0) {
|
||||
// check if all chars are •
|
||||
if (e.target.value.split("").every((char) => char === "•")) {
|
||||
setErrorData({
|
||||
title: `Invalid characters: ${e.target.value}`,
|
||||
list: [
|
||||
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
|
||||
],
|
||||
});
|
||||
}
|
||||
<>
|
||||
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
|
||||
<PopoverAnchor>
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onInputLostFocus}
|
||||
value={
|
||||
selectedOption !== "" || !onChange ? selectedOption : value
|
||||
}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
(selectedOption !== "" || !onChange) && setShowOptions(true);
|
||||
}}
|
||||
required={required}
|
||||
className={classNames(
|
||||
password &&
|
||||
selectedOption === "" &&
|
||||
!pwdVisible &&
|
||||
value !== ""
|
||||
? " text-clip password "
|
||||
: "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && selectedOption === "" && editNode ? "pr-8" : "",
|
||||
password && selectedOption === "" && !editNode ? "pr-10" : "",
|
||||
className!
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
// if the user copies a password from another input
|
||||
// it might come as ••••••••••• it causes errors
|
||||
// in ascii encoding, so we need to handle it
|
||||
if (password) {
|
||||
// check if all chars are •
|
||||
if (
|
||||
e.target.value.split("").every((char) => char === "•") && e.target.value !== ""
|
||||
) {
|
||||
setErrorData({
|
||||
title: `Invalid characters: ${e.target.value}`,
|
||||
list: [
|
||||
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
onChange && onChange(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter")
|
||||
refInput.current?.blur();
|
||||
}}
|
||||
data-testid={editNode ? id + "-edit" : id}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContentWithoutPortal
|
||||
className="nocopy nopan nodelete nodrag noundo p-0"
|
||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
||||
side="bottom"
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.includes(search) || value.includes("doNotFilter-"))
|
||||
return 1; // ensures items arent filtered
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder={optionsPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandGroup defaultChecked={false}>
|
||||
{options.map((option, id) => (
|
||||
<CommandItem
|
||||
className="group"
|
||||
key={option + id}
|
||||
value={option}
|
||||
onSelect={(currentValue) => {
|
||||
setSelectedOption &&
|
||||
setSelectedOption(
|
||||
currentValue === selectedOption
|
||||
? ""
|
||||
: currentValue
|
||||
);
|
||||
setShowOptions(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4 text-primary",
|
||||
selectedOption === option
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{option}
|
||||
</div>
|
||||
{optionButton && optionButton(option)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
{optionsButton && optionsButton}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</Popover>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute inset-y-0 h-full w-full cursor-pointer",
|
||||
selectedOption !== "" || !onChange ? "" : "hidden"
|
||||
)}
|
||||
onClick={
|
||||
selectedOption !== "" || !onChange
|
||||
? (e) => {
|
||||
setShowOptions((old) => !old);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
: () => {}
|
||||
}
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
data-testid={editNode ? id + "-edit" : id}
|
||||
/>
|
||||
></div>
|
||||
</>
|
||||
)}
|
||||
{password && (
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
password && selectedOption === "" ? "right-8" : "right-0",
|
||||
"absolute inset-y-0 flex items-center pr-2.5"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowOptions(!showOptions);
|
||||
}}
|
||||
className={cn(
|
||||
selectedOption !== ""
|
||||
? "text-medium-indigo"
|
||||
: "text-muted-foreground",
|
||||
"hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={optionsIcon}
|
||||
className={"h-4 w-4"}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{password && selectedOption === "" && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className={classNames(
|
||||
"mb-px",
|
||||
editNode
|
||||
? "input-component-true-button"
|
||||
: "input-component-false-button"
|
||||
|
|
@ -125,6 +261,7 @@ export default function InputComponent({
|
|||
}}
|
||||
>
|
||||
{password &&
|
||||
selectedOption === "" &&
|
||||
(pwdVisible ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
134
src/frontend/src/components/inputGlobalComponent/index.tsx
Normal file
134
src/frontend/src/components/inputGlobalComponent/index.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { useEffect } from "react";
|
||||
import { deleteGlobalVariable } from "../../controllers/API";
|
||||
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariables";
|
||||
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||
import { cn } from "../../utils/utils";
|
||||
import AddNewVariableButton from "../addNewVariableButtonComponent/addNewVariableButton";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import InputComponent from "../inputComponent";
|
||||
import { CommandItem } from "../ui/command";
|
||||
import { InputGlobalComponentType } from "../../types/components";
|
||||
|
||||
export default function InputGlobalComponent({
|
||||
disabled,
|
||||
onChange,
|
||||
setDb,
|
||||
name,
|
||||
data,
|
||||
editNode = false,
|
||||
}: InputGlobalComponentType): JSX.Element {
|
||||
const globalVariablesEntries = useGlobalVariablesStore(
|
||||
(state) => state.globalVariablesEntries
|
||||
);
|
||||
|
||||
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
|
||||
const removeGlobalVariable = useGlobalVariablesStore(
|
||||
(state) => state.removeGlobalVariable
|
||||
);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.node?.template[name])
|
||||
if (
|
||||
!globalVariablesEntries.includes(data.node?.template[name].value) &&
|
||||
data.node?.template[name].load_from_db
|
||||
) {
|
||||
onChange("");
|
||||
setDb(false);
|
||||
}
|
||||
}, [globalVariablesEntries]);
|
||||
|
||||
function handleDelete(key: string) {
|
||||
const id = getVariableId(key);
|
||||
if (id !== undefined) {
|
||||
deleteGlobalVariable(id)
|
||||
.then((_) => {
|
||||
removeGlobalVariable(key);
|
||||
if (
|
||||
data?.node?.template[name].value === key &&
|
||||
data?.node?.template[name].load_from_db
|
||||
) {
|
||||
onChange("");
|
||||
setDb(false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
let responseError = error as ResponseErrorDetailAPI;
|
||||
setErrorData({
|
||||
title: "Error deleting variable",
|
||||
list: [responseError.response.data.detail ?? "Unknown error"],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Error deleting variable",
|
||||
list: [cn("ID not found for variable: ", key)],
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<InputComponent
|
||||
id={"input-" + name}
|
||||
editNode={editNode}
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
options={globalVariablesEntries}
|
||||
optionsPlaceholder={"Global Variables"}
|
||||
optionsIcon="Globe"
|
||||
optionsButton={
|
||||
<AddNewVariableButton>
|
||||
<CommandItem value="doNotFilter-addNewVariable">
|
||||
<ForwardedIconComponent
|
||||
name="Plus"
|
||||
className={cn("mr-2 h-4 w-4 text-primary")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>Add New Variable</span>
|
||||
</CommandItem>
|
||||
</AddNewVariableButton>
|
||||
}
|
||||
optionButton={(option) => (
|
||||
<DeleteConfirmationModal
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleDelete(option);
|
||||
}}
|
||||
description={'variable "' + option + '"'}
|
||||
asChild
|
||||
>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="pr-1"
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</DeleteConfirmationModal>
|
||||
)}
|
||||
selectedOption={
|
||||
data?.node?.template[name].load_from_db ?? false
|
||||
? data?.node?.template[name].value
|
||||
: ""
|
||||
}
|
||||
setSelectedOption={(value) => {
|
||||
onChange(value);
|
||||
setDb(value !== "" ? true : false);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
setDb(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
|
|||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,21 @@ const PopoverContent = React.forwardRef<
|
|||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
const PopoverContentWithoutPortal = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
PopoverContentWithoutPortal.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverContent, PopoverContentWithoutPortal, PopoverTrigger };
|
||||
|
|
|
|||
|
|
@ -859,6 +859,50 @@ export async function requestLogout() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getGlobalVariables(): Promise<{
|
||||
[key: string]: { id: string; type: string };
|
||||
}> {
|
||||
const globalVariables = {};
|
||||
(await api.get(`${BASE_URL_API}variables/`)).data.forEach((element) => {
|
||||
globalVariables[element.name] = {
|
||||
id: element.id,
|
||||
type: element.type,
|
||||
};
|
||||
});
|
||||
return globalVariables;
|
||||
}
|
||||
|
||||
export async function registerGlobalVariable({
|
||||
name,
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
name: string;
|
||||
value: string;
|
||||
type?: string;
|
||||
}): Promise<AxiosResponse<{ name: string; id: string; type: string }>> {
|
||||
return await api.post(`${BASE_URL_API}variables/`, {
|
||||
name,
|
||||
value,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteGlobalVariable(id: string) {
|
||||
api.delete(`${BASE_URL_API}variables/${id}`);
|
||||
}
|
||||
|
||||
export async function updateGlobalVariable(
|
||||
name: string,
|
||||
value: string,
|
||||
id: string
|
||||
) {
|
||||
api.patch(`${BASE_URL_API}variables/${id}`, {
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getVerticesOrder(
|
||||
flowId: string,
|
||||
startNodeId?: string | null,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,18 @@ export default function DeleteConfirmationModal({
|
|||
children,
|
||||
onConfirm,
|
||||
description,
|
||||
asChild,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
onConfirm: () => void;
|
||||
onConfirm: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
description?: string;
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger tabIndex={-1}>{children}</DialogTrigger>
|
||||
<DialogTrigger asChild={asChild} tabIndex={-1}>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
|
|
@ -39,16 +43,21 @@ export default function DeleteConfirmationModal({
|
|||
Note: This action is irreversible.
|
||||
</span>
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
<Button className="mr-3" variant="outline">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mr-3"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClick={(e) => {
|
||||
onConfirm(e);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import DictComponent from "../../components/dictComponent";
|
|||
import Dropdown from "../../components/dropdownComponent";
|
||||
import FloatComponent from "../../components/floatComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import InputFileComponent from "../../components/inputFileComponent";
|
||||
import InputGlobalComponent from "../../components/inputGlobalComponent";
|
||||
import InputListComponent from "../../components/inputListComponent";
|
||||
import IntComponent from "../../components/intComponent";
|
||||
import KeypairListComponent from "../../components/keypairListComponent";
|
||||
|
|
@ -28,7 +28,9 @@ import {
|
|||
LANGFLOW_SUPPORTED_TYPES,
|
||||
limitScrollFieldsModal,
|
||||
} from "../../constants/constants";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { useGlobalVariablesStore } from "../../stores/globalVariables";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import {
|
||||
convertObjToArray,
|
||||
|
|
@ -58,6 +60,10 @@ const EditNodeModal = forwardRef(
|
|||
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
||||
const globalVariablesEntries = useGlobalVariablesStore(
|
||||
(state) => state.globalVariablesEntries
|
||||
);
|
||||
|
||||
function changeAdvanced(n) {
|
||||
setMyData((old) => {
|
||||
|
|
@ -253,28 +259,23 @@ const EditNodeModal = forwardRef(
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={
|
||||
"input-" +
|
||||
myData.node.template[templateParam]
|
||||
.name
|
||||
}
|
||||
editNode={true}
|
||||
<InputGlobalComponent
|
||||
disabled={disabled}
|
||||
password={
|
||||
myData.node.template[templateParam]
|
||||
.password ?? false
|
||||
editNode={true}
|
||||
onChange={(value) =>
|
||||
handleOnNewValue(value, templateParam)
|
||||
}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(
|
||||
value,
|
||||
templateParam
|
||||
);
|
||||
setDb={(value) => {
|
||||
setMyData((oldData) => {
|
||||
let newData = cloneDeep(oldData);
|
||||
newData.node!.template[
|
||||
templateParam
|
||||
].load_from_db = value;
|
||||
return newData;
|
||||
});
|
||||
}}
|
||||
name={templateParam}
|
||||
data={myData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
34
src/frontend/src/stores/globalVariables.ts
Normal file
34
src/frontend/src/stores/globalVariables.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { create } from "zustand";
|
||||
import { GlobalVariablesStore } from "../types/zustand/globalVariables";
|
||||
|
||||
export const useGlobalVariablesStore = create<GlobalVariablesStore>(
|
||||
(set, get) => ({
|
||||
globalVariablesEntries: [],
|
||||
globalVariables: {},
|
||||
setGlobalVariables: (variables) => {
|
||||
set({
|
||||
globalVariables: variables,
|
||||
globalVariablesEntries: Object.keys(variables),
|
||||
});
|
||||
},
|
||||
addGlobalVariable: (name, id, type) => {
|
||||
const data = { id, type };
|
||||
const newVariables = { ...get().globalVariables, [name]: data };
|
||||
set({
|
||||
globalVariables: newVariables,
|
||||
globalVariablesEntries: Object.keys(newVariables),
|
||||
});
|
||||
},
|
||||
removeGlobalVariable: (name) => {
|
||||
const newVariables = { ...get().globalVariables };
|
||||
delete newVariables[name];
|
||||
set({
|
||||
globalVariables: newVariables,
|
||||
globalVariablesEntries: Object.keys(newVariables),
|
||||
});
|
||||
},
|
||||
getVariableId: (name) => {
|
||||
return get().globalVariables[name]?.id;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
@ -450,7 +450,7 @@
|
|||
@apply text-muted-foreground;
|
||||
}
|
||||
.dropdown-component-arrow {
|
||||
@apply pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2;
|
||||
@apply pointer-events-none absolute inset-y-0 flex items-center pr-2;
|
||||
}
|
||||
.dropdown-component-arrow-color {
|
||||
@apply h-5 w-5 text-accent-foreground;
|
||||
|
|
|
|||
|
|
@ -177,3 +177,6 @@ export type CodeErrorDataTypeAPI = {
|
|||
export type ResponseErrorTypeAPI = {
|
||||
response: { data: { detail: CodeErrorDataTypeAPI } };
|
||||
};
|
||||
export type ResponseErrorDetailAPI = {
|
||||
response: { data: { detail: string } };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { sourceHandleType, targetHandleType } from "./../flow/index";
|
|||
export type InputComponentType = {
|
||||
autoFocus?: boolean;
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
||||
value: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
password: boolean;
|
||||
required?: boolean;
|
||||
isForm?: boolean;
|
||||
|
|
@ -20,6 +20,13 @@ export type InputComponentType = {
|
|||
className?: string;
|
||||
id?: string;
|
||||
blurOnEnter?: boolean;
|
||||
optionsIcon?: string;
|
||||
optionsPlaceholder?: string;
|
||||
options?: string[];
|
||||
optionsButton?: ReactElement;
|
||||
optionButton?: (option: string) => ReactElement;
|
||||
selectedOption?: string;
|
||||
setSelectedOption?: (value: string) => void;
|
||||
};
|
||||
export type ToggleComponentType = {
|
||||
enabled: boolean;
|
||||
|
|
@ -64,6 +71,16 @@ export type InputListComponentType = {
|
|||
editNode?: boolean;
|
||||
};
|
||||
|
||||
export type InputGlobalComponentType =
|
||||
{
|
||||
disabled: boolean,
|
||||
onChange: (value: string) => void,
|
||||
setDb: (value: boolean) => void,
|
||||
name: string,
|
||||
data: NodeDataType,
|
||||
editNode?: boolean,
|
||||
};
|
||||
|
||||
export type KeyPairListComponentType = {
|
||||
value: any;
|
||||
onChange: (value: Object[]) => void;
|
||||
|
|
|
|||
10
src/frontend/src/types/zustand/globalVariables/index.ts
Normal file
10
src/frontend/src/types/zustand/globalVariables/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export type GlobalVariablesStore = {
|
||||
globalVariablesEntries: Array<string>;
|
||||
globalVariables: { [name: string]: { id: string; type?: string } };
|
||||
setGlobalVariables: (variables: {
|
||||
[name: string]: { id: string; type?: string };
|
||||
}) => void;
|
||||
addGlobalVariable: (name: string, id: string, type?: string) => void;
|
||||
removeGlobalVariable: (name: string) => void;
|
||||
getVariableId: (name: string) => string | undefined;
|
||||
};
|
||||
|
|
@ -58,6 +58,7 @@ import {
|
|||
GitBranchPlus,
|
||||
GitFork,
|
||||
GithubIcon,
|
||||
Globe,
|
||||
Group,
|
||||
Hammer,
|
||||
Heart,
|
||||
|
|
@ -418,6 +419,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
PlusCircle,
|
||||
PlusSquare,
|
||||
Code2,
|
||||
Globe,
|
||||
Variable,
|
||||
Snowflake,
|
||||
Store,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue