Merge remote-tracking branch 'origin/dev' into chatImg

This commit is contained in:
ogabrielluiz 2024-06-06 09:34:13 -03:00
commit 8b1234f6ca
598 changed files with 26867 additions and 22399 deletions

View file

@ -22,7 +22,8 @@ from sqlmodel import select
from langflow.main import setup_app
from langflow.services.database.models.folder.utils import create_default_folder_if_it_doesnt_exist
from langflow.services.database.utils import session_getter
from langflow.services.deps import get_db_service
from langflow.services.deps import get_db_service, get_settings_service, session_scope
from langflow.services.settings.constants import DEFAULT_SUPERUSER
from langflow.services.utils import initialize_services
from langflow.utils.logger import configure, logger
from langflow.utils.util import update_settings
@ -76,14 +77,13 @@ def set_var_for_macos_issue():
def run(
host: str = typer.Option("127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"),
workers: int = typer.Option(1, help="Number of worker processes.", envvar="LANGFLOW_WORKERS"),
timeout: int = typer.Option(300, help="Worker timeout in seconds."),
timeout: int = typer.Option(300, help="Worker timeout in seconds.", envvar="LANGFLOW_WORKER_TIMEOUT"),
port: int = typer.Option(7860, help="Port to listen on.", envvar="LANGFLOW_PORT"),
components_path: Optional[Path] = typer.Option(
Path(__file__).parent / "components",
help="Path to the directory containing custom components.",
envvar="LANGFLOW_COMPONENTS_PATH",
),
config: str = typer.Option(Path(__file__).parent / "config.yaml", help="Path to the configuration file."),
# .env file param
env_file: Path = typer.Option(None, help="Path to the .env file containing environment variables."),
log_level: str = typer.Option("critical", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
@ -121,7 +121,7 @@ def run(
),
):
"""
Run the Langflow.
Run Langflow.
"""
configure(log_level=log_level, log_file=log_file)
@ -132,7 +132,6 @@ def run(
load_dotenv(env_file, override=True)
update_settings(
config,
dev=dev,
remove_api_keys=remove_api_keys,
cache=cache,
@ -146,6 +145,10 @@ def run(
if is_port_in_use(port, host):
port = get_free_port(port)
settings_service = get_settings_service()
settings_service.set("worker_timeout", timeout)
options = {
"bind": f"{host}:{port}",
"workers": get_number_of_workers(workers),
@ -510,6 +513,66 @@ def migration(
display_results(results)
@app.command()
def api_key(
log_level: str = typer.Option("error", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
):
"""
Creates an API key for the default superuser if AUTO_LOGIN is enabled.
Args:
log_level (str, optional): Logging level. Defaults to "error".
Returns:
None
"""
configure(log_level=log_level)
initialize_services()
settings_service = get_settings_service()
auth_settings = settings_service.auth_settings
if not auth_settings.AUTO_LOGIN:
typer.echo("Auto login is disabled. API keys cannot be created through the CLI.")
return
with session_scope() as session:
from langflow.services.database.models.user.model import User
superuser = session.exec(select(User).where(User.username == DEFAULT_SUPERUSER)).first()
if not superuser:
typer.echo("Default superuser not found. This command requires a superuser and AUTO_LOGIN to be enabled.")
return
from langflow.services.database.models.api_key import ApiKey, ApiKeyCreate
from langflow.services.database.models.api_key.crud import create_api_key, delete_api_key
api_key = session.exec(select(ApiKey).where(ApiKey.user_id == superuser.id)).first()
if api_key:
delete_api_key(session, api_key.id)
api_key_create = ApiKeyCreate(name="CLI")
unmasked_api_key = create_api_key(session, api_key_create, user_id=superuser.id)
session.commit()
# Create a banner to display the API key and tell the user it won't be shown again
api_key_banner(unmasked_api_key)
def api_key_banner(unmasked_api_key):
is_mac = platform.system() == "Darwin"
import pyperclip # type: ignore
pyperclip.copy(unmasked_api_key.api_key)
panel = Panel(
f"[bold]API Key Created Successfully:[/bold]\n\n"
f"[bold blue]{unmasked_api_key.api_key}[/bold blue]\n\n"
"This is the only time the API key will be displayed. \n"
"Make sure to store it in a secure location. \n\n"
f"The API key has been copied to your clipboard. [bold]{['Ctrl','Cmd'][is_mac]} + V[/bold] to paste it.",
box=box.ROUNDED,
border_style="blue",
expand=False,
)
console = Console()
console.print(panel)
def main():
with warnings.catch_warnings():
warnings.simplefilter("ignore")

View file

@ -11,6 +11,7 @@ from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.engine.reflection import Inspector
from langflow.utils import migration
${imports if imports else ""}
# revision identifiers, used by Alembic.
@ -22,13 +23,9 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
conn = op.get_bind()
inspector = Inspector.from_engine(conn) # type: ignore
table_names = inspector.get_table_names()
${upgrades if upgrades else "pass"}
def downgrade() -> None:
conn = op.get_bind()
inspector = Inspector.from_engine(conn) # type: ignore
table_names = inspector.get_table_names()
${downgrades if downgrades else "pass"}

View file

@ -48,10 +48,12 @@ def upgrade() -> None:
with op.batch_alter_table("folder", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_folder_name"), ["name"], unique=False)
if "folder_id" not in inspector.get_columns("flow"):
with op.batch_alter_table("flow", schema=None) as batch_op:
column_names = [column["name"] for column in inspector.get_columns("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "folder_id" not in column_names:
batch_op.add_column(sa.Column("folder_id", sqlmodel.sql.sqltypes.GUID(), nullable=True))
batch_op.create_foreign_key("flow_folder_id_fkey", "folder", ["folder_id"], ["id"])
if "folder" in column_names:
batch_op.drop_column("folder")
# ### end Alembic commands ###
@ -62,11 +64,13 @@ def downgrade() -> None:
inspector = Inspector.from_engine(conn) # type: ignore
table_names = inspector.get_table_names()
# ### commands auto generated by Alembic - please adjust! ###
if "folder_id" in inspector.get_columns("flow"):
with op.batch_alter_table("flow", schema=None) as batch_op:
column_names = [column["name"] for column in inspector.get_columns("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "folder" not in column_names:
batch_op.add_column(sa.Column("folder", sa.VARCHAR(), nullable=True))
batch_op.drop_constraint("flow_folder_id_fkey", type_="foreignkey")
if "folder_id" in column_names:
batch_op.drop_column("folder_id")
batch_op.drop_constraint("flow_folder_id_fkey", type_="foreignkey")
indexes = inspector.get_indexes("folder")
if "ix_folder_name" in [index["name"] for index in indexes]:

View file

@ -0,0 +1,42 @@
"""Add unique constraints per user in folder table
Revision ID: 1c79524817ed
Revises: 3bb0ddf32dfb
Create Date: 2024-05-29 23:12:09.146880
"""
from typing import Sequence, Union
from alembic import op
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision: str = "1c79524817ed"
down_revision: Union[str, None] = "3bb0ddf32dfb"
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
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("folder")]
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("folder", schema=None) as batch_op:
if "unique_folder_name" not in constraints_names:
batch_op.create_unique_constraint("unique_folder_name", ["user_id", "name"])
# ### end Alembic commands ###
def downgrade() -> None:
conn = op.get_bind()
inspector = Inspector.from_engine(conn) # type: ignore
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("folder")]
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("folder", schema=None) as batch_op:
if "unique_folder_name" in constraints_names:
batch_op.drop_constraint("unique_folder_name", type_="unique")
# ### end Alembic commands ###

View file

@ -0,0 +1,54 @@
"""Add unique constraints per user in flow table
Revision ID: 3bb0ddf32dfb
Revises: a72f5cf9c2f9
Create Date: 2024-05-29 23:08:43.935040
"""
from typing import Sequence, Union
from alembic import op
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision: str = "3bb0ddf32dfb"
down_revision: Union[str, None] = "a72f5cf9c2f9"
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
# ### commands auto generated by Alembic - please adjust! ###
indexes_names = [index["name"] for index in inspector.get_indexes("flow")]
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "ix_flow_endpoint_name" in indexes_names:
batch_op.drop_index("ix_flow_endpoint_name")
batch_op.create_index(batch_op.f("ix_flow_endpoint_name"), ["endpoint_name"], unique=False)
if "unique_flow_endpoint_name" not in constraints_names:
batch_op.create_unique_constraint("unique_flow_endpoint_name", ["user_id", "endpoint_name"])
if "unique_flow_name" not in constraints_names:
batch_op.create_unique_constraint("unique_flow_name", ["user_id", "name"])
# ### end Alembic commands ###
def downgrade() -> None:
conn = op.get_bind()
inspector = Inspector.from_engine(conn) # type: ignore
# ### commands auto generated by Alembic - please adjust! ###
indexes_names = [index["name"] for index in inspector.get_indexes("flow")]
constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "unique_flow_name" in constraints_names:
batch_op.drop_constraint("unique_flow_name", type_="unique")
if "unique_flow_endpoint_name" in constraints_names:
batch_op.drop_constraint("unique_flow_endpoint_name", type_="unique")
if "ix_flow_endpoint_name" in indexes_names:
batch_op.drop_index(batch_op.f("ix_flow_endpoint_name"))
batch_op.create_index("ix_flow_endpoint_name", ["endpoint_name"], unique=1)
# ### end Alembic commands ###

View file

@ -0,0 +1,45 @@
"""Add webhook columns
Revision ID: 631faacf5da2
Revises: 1c79524817ed
Create Date: 2024-04-22 15:14:43.454784
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision: str = "631faacf5da2"
down_revision: Union[str, None] = "1c79524817ed"
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! ###
column_names = [column["name"] for column in inspector.get_columns("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "flow" in table_names and "webhook" not in column_names:
batch_op.add_column(sa.Column("webhook", sa.Boolean(), nullable=True))
# ### 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! ###
column_names = [column["name"] for column in inspector.get_columns("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "flow" in table_names and "webhook" in column_names:
batch_op.drop_column("webhook")
# ### end Alembic commands ###

View file

@ -52,9 +52,14 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
try:
conn = op.get_bind()
inspector = Inspector.from_engine(conn) # type: ignore
column_names = [column["name"] for column in inspector.get_columns("flow")]
with op.batch_alter_table("flow", schema=None) as batch_op:
batch_op.drop_column("folder")
batch_op.drop_column("updated_at")
if "folder" in column_names:
batch_op.drop_column("folder")
if "updated_at" in column_names:
batch_op.drop_column("updated_at")
except Exception as e:
print(e)
pass

View file

@ -0,0 +1,52 @@
"""Add endpoint name col
Revision ID: a72f5cf9c2f9
Revises: 29fe8f1f806b
Create Date: 2024-05-29 21:44:04.240816
"""
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 = "a72f5cf9c2f9"
down_revision: Union[str, None] = "29fe8f1f806b"
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
# ### commands auto generated by Alembic - please adjust! ###
column_names = [column["name"] for column in inspector.get_columns("flow")]
indexes = inspector.get_indexes("flow")
index_names = [index["name"] for index in indexes]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "endpoint_name" not in column_names:
batch_op.add_column(sa.Column("endpoint_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
if "ix_flow_endpoint_name" not in index_names:
batch_op.create_index(batch_op.f("ix_flow_endpoint_name"), ["endpoint_name"], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
conn = op.get_bind()
inspector = Inspector.from_engine(conn) # type: ignore
# ### commands auto generated by Alembic - please adjust! ###
column_names = [column["name"] for column in inspector.get_columns("flow")]
indexes = inspector.get_indexes("flow")
index_names = [index["name"] for index in indexes]
with op.batch_alter_table("flow", schema=None) as batch_op:
if "ix_flow_endpoint_name" in index_names:
batch_op.drop_index(batch_op.f("ix_flow_endpoint_name"))
if "endpoint_name" in column_names:
batch_op.drop_column("endpoint_name")
# ### end Alembic commands ###

View file

@ -1,4 +1,3 @@
import os
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Optional
@ -287,7 +286,7 @@ async def get_next_runnable_vertices(
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
graph.vertices_to_run.remove(v_id)
graph.remove_from_predecessors(v_id)
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
await chat_service.set_cache(key=flow_id, data=graph, lock=lock)
return next_runnable_vertices

View file

@ -1,13 +1,12 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from uuid import UUID
from langchain.schema import AgentAction, AgentFinish
from langchain_core.callbacks.base import AsyncCallbackHandler
from loguru import logger
from langflow.api.v1.schemas import ChatResponse, PromptResponse
from langflow.services.deps import get_chat_service, get_socket_service
from langflow.utils.util import remove_ansi_escape_codes
from langchain_core.agents import AgentAction, AgentFinish
if TYPE_CHECKING:
from langflow.services.socket.service import SocketIOService

View file

@ -1,6 +1,5 @@
import time
import uuid
from functools import partial
from typing import TYPE_CHECKING, Annotated, Optional
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
@ -164,7 +163,6 @@ async def build_vertex(
vertex = graph.get_vertex(vertex_id)
try:
lock = chat_service._cache_locks[flow_id_str]
set_cache_coro = partial(chat_service.set_cache, flow_id=flow_id_str)
(
next_runnable_vertices,
top_level_vertices,
@ -175,7 +173,7 @@ async def build_vertex(
vertex,
) = await graph.build_vertex(
lock=lock,
set_cache_coro=set_cache_coro,
chat_service=chat_service,
vertex_id=vertex_id,
user_id=current_user.id,
inputs_dict=inputs.model_dump() if inputs else {},

View file

@ -1,14 +1,15 @@
from http import HTTPStatus
from typing import Annotated, List, Optional, Union
from typing import TYPE_CHECKING, Annotated, List, Optional, Union
from uuid import UUID
import sqlalchemy as sa
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Request, UploadFile, status
from loguru import logger
from sqlmodel import Session, select
from langflow.api.utils import update_frontend_node_with_template_values
from langflow.api.v1.schemas import (
ConfigResponse,
CustomComponentRequest,
InputValueRequest,
ProcessResponse,
@ -18,21 +19,25 @@ from langflow.api.v1.schemas import (
UpdateCustomComponentRequest,
UploadFileResponse,
)
from langflow.custom import CustomComponent
from langflow.custom.utils import build_custom_component_template
from langflow.graph.graph.base import Graph
from langflow.graph.schema import RunOutputs
from langflow.interface.custom.custom_component import CustomComponent
from langflow.interface.custom.utils import build_custom_component_template
from langflow.helpers.flow import get_flow_by_id_or_endpoint_name
from langflow.processing.process import process_tweaks, run_graph_internal
from langflow.schema.graph import Tweaks
from langflow.services.auth.utils import api_key_security, get_current_active_user
from langflow.services.cache.utils import save_uploaded_file
from langflow.services.database.models.flow import Flow
from langflow.services.database.models.flow.utils import get_all_webhook_components_in_flow, get_flow_by_id
from langflow.services.database.models.user.model import User
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
from langflow.services.session.service import SessionService
from langflow.services.task.service import TaskService
# build router
if TYPE_CHECKING:
from langflow.services.settings.manager import SettingsService
router = APIRouter(tags=["Base"])
@ -44,17 +49,77 @@ def get_all(
logger.debug("Building langchain types dict")
try:
all_types_dict = get_all_types_dict(settings_service.settings.COMPONENTS_PATH)
all_types_dict = get_all_types_dict(settings_service.settings.components_path)
return all_types_dict
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
async def simple_run_flow(
db: Session,
flow: Flow,
input_request: SimplifiedAPIRequest,
session_service: SessionService,
stream: bool = False,
api_key_user: Optional[User] = None,
):
try:
task_result: List[RunOutputs] = []
artifacts = {}
user_id = api_key_user.id if api_key_user else None
flow_id_str = str(flow.id)
if input_request.session_id:
session_data = await session_service.load_session(input_request.session_id, flow_id=flow_id_str)
graph, artifacts = session_data if session_data else (None, None)
if graph is None:
raise ValueError(f"Session {input_request.session_id} not found")
else:
if flow.data is None:
raise ValueError(f"Flow {flow_id_str} has no data")
graph_data = flow.data
graph_data = process_tweaks(graph_data, input_request.tweaks or {}, stream=stream)
graph = Graph.from_payload(graph_data, flow_id=flow_id_str, user_id=str(user_id))
inputs = [
InputValueRequest(components=[], input_value=input_request.input_value, type=input_request.input_type)
]
if input_request.output_component:
outputs = [input_request.output_component]
else:
outputs = [
vertex.id
for vertex in graph.vertices
if input_request.output_type == "debug"
or (
vertex.is_output
and (input_request.output_type == "any" or input_request.output_type in vertex.id.lower())
)
]
task_result, session_id = await run_graph_internal(
graph=graph,
flow_id=flow_id_str,
session_id=input_request.session_id,
inputs=inputs,
outputs=outputs,
artifacts=artifacts,
session_service=session_service,
stream=stream,
)
return RunResponse(outputs=task_result, session_id=session_id)
except sa.exc.StatementError as exc:
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc):
logger.error(f"Flow ID {flow_id_str} is not a valid UUID")
# This means the Flow ID is not a valid UUID which means it can't find the flow
raise ValueError(str(exc)) from exc
@router.post("/run/{flow_id_or_name}", response_model=RunResponse, response_model_exclude_none=True)
async def simplified_run_flow(
db: Annotated[Session, Depends(get_session)],
flow_id: UUID,
flow: Annotated[Flow, Depends(get_flow_by_id_or_endpoint_name)],
input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(),
stream: bool = False,
api_key_user: User = Depends(api_key_security),
@ -65,7 +130,7 @@ async def simplified_run_flow(
### Parameters:
- `db` (Session): Database session for executing queries.
- `flow_id` (str): Unique identifier of the flow to be executed.
- `flow_id_or_name` (str): ID or endpoint name of the flow to run.
- `input_request` (SimplifiedAPIRequest): Request object containing input values, types, output selection, tweaks, and session ID.
- `api_key_user` (User): User object derived from the provided API key, used for authentication.
- `session_service` (SessionService): Service for managing flow sessions, essential for session reuse and caching.
@ -108,73 +173,21 @@ async def simplified_run_flow(
This endpoint provides a powerful interface for executing flows with enhanced flexibility and efficiency, supporting a wide range of applications by allowing for dynamic input and output configuration along with performance optimizations through session management and caching.
"""
session_id = input_request.session_id
try:
flow_id_str = str(flow_id)
artifacts = {}
if input_request.session_id:
session_data = await session_service.load_session(input_request.session_id, flow_id=flow_id_str)
graph, artifacts = session_data if session_data else (None, None)
if graph is None:
raise ValueError(f"Session {input_request.session_id} not found")
else:
# Get the flow that matches the flow_id and belongs to the user
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
flow = db.exec(select(Flow).where(Flow.id == flow_id_str).where(Flow.user_id == api_key_user.id)).first()
if flow is None:
raise ValueError(f"Flow {flow_id_str} not found")
if flow.data is None:
raise ValueError(f"Flow {flow_id_str} has no data")
graph_data = flow.data
graph_data = process_tweaks(graph_data, input_request.tweaks or {}, stream=stream)
graph = Graph.from_payload(graph_data, flow_id=flow_id_str, user_id=str(api_key_user.id))
inputs = [
InputValueRequest(components=[], input_value=input_request.input_value, type=input_request.input_type)
]
# outputs is a list of all components that should return output
# we need to get them by checking their type
# if the output type is debug, we return all outputs
# if the output type is any, we return all outputs that are either chat or text
# if the output type is chat or text, we return only the outputs that match the type
if input_request.output_component:
outputs = [input_request.output_component]
else:
outputs = [
vertex.id
for vertex in graph.vertices
if input_request.output_type == "debug"
or (
vertex.is_output
and (input_request.output_type == "any" or input_request.output_type in vertex.id.lower())
)
]
task_result, session_id = await run_graph_internal(
graph=graph,
flow_id=flow_id_str,
session_id=input_request.session_id,
inputs=inputs,
outputs=outputs,
artifacts=artifacts,
return await simple_run_flow(
db=db,
flow=flow,
input_request=input_request,
session_service=session_service,
stream=stream,
api_key_user=api_key_user,
)
return RunResponse(outputs=task_result, session_id=session_id)
except sa.exc.StatementError as exc:
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc):
logger.error(f"Flow ID {flow_id_str} is not a valid UUID")
# This means the Flow ID is not a valid UUID which means it can't find the flow
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except ValueError as exc:
if f"Flow {flow_id_str} not found" in str(exc):
logger.error(f"Flow {flow_id_str} not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
elif f"Session {session_id} not found" in str(exc):
logger.error(f"Session {session_id} not found")
if "badly formed hexadecimal UUID string" in str(exc):
# This means the Flow ID is not a valid UUID which means it can't find the flow
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
if "not found" in str(exc):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
else:
logger.exception(exc)
@ -184,6 +197,68 @@ async def simplified_run_flow(
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
@router.post("/webhook/{flow_id}", response_model=dict, status_code=HTTPStatus.ACCEPTED)
async def webhook_run_flow(
db: Annotated[Session, Depends(get_session)],
flow: Annotated[Flow, Depends(get_flow_by_id)],
request: Request,
background_tasks: BackgroundTasks,
session_service: SessionService = Depends(get_session_service),
):
"""
Run a flow using a webhook request.
Args:
db (Session): The database session.
request (Request): The incoming HTTP request.
background_tasks (BackgroundTasks): The background tasks manager.
session_service (SessionService, optional): The session service. Defaults to Depends(get_session_service).
flow (Flow, optional): The flow to be executed. Defaults to Depends(get_flow_by_id).
Returns:
dict: A dictionary containing the status of the task.
Raises:
HTTPException: If the flow is not found or if there is an error processing the request.
"""
try:
logger.debug("Received webhook request")
data = await request.body()
if not data:
logger.error("Request body is empty")
raise ValueError(
"Request body is empty. You should provide a JSON payload containing the flow ID.",
)
# get all webhook components in the flow
webhook_components = get_all_webhook_components_in_flow(flow.data)
tweaks = {}
data_dict = await request.json()
for component in webhook_components:
tweaks[component["id"]] = {"data": data.decode() if isinstance(data, bytes) else data}
input_request = SimplifiedAPIRequest(
input_value=data_dict.get("input_value", ""),
input_type=data_dict.get("input_type", "chat"),
output_type=data_dict.get("output_type", "chat"),
tweaks=tweaks,
session_id=data_dict.get("session_id"),
)
logger.debug("Starting background task")
background_tasks.add_task(
simple_run_flow,
db=db,
flow=flow,
input_request=input_request,
session_service=session_service,
)
return {"message": "Task started in the background", "status": "in progress"}
except Exception as exc:
if "Flow ID is required" in str(exc) or "Request body is empty" in str(exc):
raise HTTPException(status_code=400, detail=str(exc)) from exc
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/run/advanced/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
async def experimental_run_flow(
session: Annotated[Session, Depends(get_session)],
@ -441,3 +516,15 @@ async def custom_component_update(
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.get("/config", response_model=ConfigResponse)
def get_config():
try:
from langflow.services.deps import get_settings_service
settings_service: "SettingsService" = get_settings_service()
return settings_service.settings.model_dump()
except Exception as exc:
logger.exception(exc)
raise HTTPException(status_code=500, detail=str(exc)) from exc

View file

@ -9,10 +9,11 @@ from loguru import logger
from sqlmodel import Session, col, select
from langflow.api.utils import remove_api_keys, validate_is_component
from langflow.api.v1.schemas import FlowListCreate, FlowListIds, FlowListRead
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
from langflow.services.database.models.flow.utils import get_webhook_component_in_flow
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
from langflow.services.database.models.folder.model import Folder
from langflow.services.database.models.user.model import User
@ -38,7 +39,10 @@ def create_flow(
db_flow.updated_at = datetime.now(timezone.utc)
if db_flow.folder_id is None:
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
# Make sure flows always have a folder
default_folder = session.exec(
select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME, Folder.user_id == current_user.id)
).first()
if default_folder:
db_flow.folder_id = default_folder.id
@ -54,8 +58,22 @@ def read_flows(
current_user: User = Depends(get_current_active_user),
session: Session = Depends(get_session),
settings_service: "SettingsService" = Depends(get_settings_service),
remove_example_flows: bool = False,
):
"""Read all flows."""
"""
Retrieve a list of flows.
Args:
current_user (User): The current authenticated user.
session (Session): The database session.
settings_service (SettingsService): The settings service.
remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
Returns:
List[Dict]: A list of flows in JSON format.
"""
try:
auth_settings = settings_service.auth_settings
if auth_settings.AUTO_LOGIN:
@ -70,15 +88,16 @@ def read_flows(
flows = validate_is_component(flows) # type: ignore
flow_ids = [flow.id for flow in flows]
# with the session get the flows that DO NOT have a user_id
try:
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
if not remove_example_flows:
try:
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
example_flows = folder.flows if folder else []
for example_flow in example_flows:
if example_flow.id not in flow_ids:
flows.append(example_flow) # type: ignore
except Exception as e:
logger.error(e)
example_flows = folder.flows if folder else []
for example_flow in example_flows:
if example_flow.id not in flow_ids:
flows.append(example_flow) # type: ignore
except Exception as e:
logger.error(e)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
return [jsonable_encoder(flow) for flow in flows]
@ -117,30 +136,51 @@ def update_flow(
settings_service=Depends(get_settings_service),
):
"""Update a flow."""
try:
db_flow = read_flow(
session=session,
flow_id=flow_id,
current_user=current_user,
settings_service=settings_service,
)
if not db_flow:
raise HTTPException(status_code=404, detail="Flow not found")
flow_data = flow.model_dump(exclude_unset=True)
if settings_service.settings.remove_api_keys:
flow_data = remove_api_keys(flow_data)
for key, value in flow_data.items():
if value is not None:
setattr(db_flow, key, value)
webhook_component = get_webhook_component_in_flow(db_flow.data)
db_flow.webhook = webhook_component is not None
db_flow.updated_at = datetime.now(timezone.utc)
if db_flow.folder_id is None:
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
if default_folder:
db_flow.folder_id = default_folder.id
session.add(db_flow)
session.commit()
session.refresh(db_flow)
return db_flow
except Exception as e:
# If it is a validation error, return the error message
if hasattr(e, "errors"):
raise HTTPException(status_code=400, detail=str(e)) from e
elif "UNIQUE constraint failed" in str(e):
# Get the name of the column that failed
columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0]
# UNIQUE constraint failed: flow.user_id, flow.name
# or UNIQUE constraint failed: flow.name
# if the column has id in it, we want the other column
column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0]
db_flow = read_flow(
session=session,
flow_id=flow_id,
current_user=current_user,
settings_service=settings_service,
)
if not db_flow:
raise HTTPException(status_code=404, detail="Flow not found")
flow_data = flow.model_dump(exclude_unset=True)
if settings_service.settings.REMOVE_API_KEYS:
flow_data = remove_api_keys(flow_data)
for key, value in flow_data.items():
if value is not None:
setattr(db_flow, key, value)
db_flow.updated_at = datetime.now(timezone.utc)
if db_flow.folder_id is None:
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
if default_folder:
db_flow.folder_id = default_folder.id
session.add(db_flow)
session.commit()
session.refresh(db_flow)
return db_flow
raise HTTPException(
status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique"
) from e
elif isinstance(e, HTTPException):
raise e
else:
raise HTTPException(status_code=500, detail=str(e)) from e
@router.delete("/{flow_id}", status_code=200)
@ -218,9 +258,9 @@ async def download_file(
return FlowListRead(flows=flows)
@router.post("/multiple_delete/")
@router.delete("/")
async def delete_multiple_flows(
flow_ids: FlowListIds, user: User = Depends(get_current_active_user), db: Session = Depends(get_session)
flow_ids: List[UUID], user: User = Depends(get_current_active_user), db: Session = Depends(get_session)
):
"""
Delete multiple flows by their IDs.
@ -234,9 +274,7 @@ async def delete_multiple_flows(
"""
try:
deleted_flows = db.exec(
select(Flow).where(col(Flow.id).in_(flow_ids.flow_ids)).where(Flow.user_id == user.id)
).all()
deleted_flows = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all()
for flow in deleted_flows:
db.delete(flow)
db.commit()

View file

@ -1,14 +1,12 @@
from typing import List
from uuid import UUID
import orjson
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
from sqlalchemy import update
from sqlalchemy import or_, update
from sqlmodel import Session, select
from langflow.api.v1.flows import create_flows
from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow.model import Flow, FlowCreate, FlowRead
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
@ -35,6 +33,18 @@ def create_folder(
try:
new_folder = Folder.model_validate(folder, from_attributes=True)
new_folder.user_id = current_user.id
folder_results = session.exec(
select(Folder).where(
Folder.name.like(f"{new_folder.name}%"), # type: ignore
Folder.user_id == current_user.id,
)
)
existing_folder_names = [folder.name for folder in folder_results]
if existing_folder_names:
new_folder.name = f"{new_folder.name} ({len(existing_folder_names) + 1})"
session.add(new_folder)
session.commit()
session.refresh(new_folder)
@ -63,16 +73,11 @@ def read_folders(
current_user: User = Depends(get_current_active_user),
):
try:
folders = session.exec(select(Folder).where(Folder.user_id == current_user.id)).all()
return folders
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/starter-projects", response_model=FolderReadWithFlows, status_code=200)
def read_starter_folders(*, session: Session = Depends(get_session)):
try:
folders = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
folders = session.exec(
select(Folder).where(
or_(Folder.user_id == current_user.id, Folder.user_id == None) # type: ignore # noqa: E711
)
).all()
return folders
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@ -82,7 +87,7 @@ def read_starter_folders(*, session: Session = Depends(get_session)):
def read_folder(
*,
session: Session = Depends(get_session),
folder_id: UUID,
folder_id: str,
current_user: User = Depends(get_current_active_user),
):
try:
@ -100,7 +105,7 @@ def read_folder(
def update_folder(
*,
session: Session = Depends(get_session),
folder_id: UUID,
folder_id: str,
folder: FolderUpdate, # Assuming FolderUpdate is a Pydantic model defining updatable fields
current_user: User = Depends(get_current_active_user),
):
@ -149,7 +154,7 @@ def update_folder(
def delete_folder(
*,
session: Session = Depends(get_session),
folder_id: UUID,
folder_id: str,
current_user: User = Depends(get_current_active_user),
):
try:
@ -171,7 +176,7 @@ def delete_folder(
async def download_file(
*,
session: Session = Depends(get_session),
folder_id: UUID,
folder_id: str,
current_user: User = Depends(get_current_active_user),
):
"""Download all flows from folder."""

View file

@ -46,6 +46,7 @@ async def login_to_get_access_token(
samesite=auth_settings.REFRESH_SAME_SITE,
secure=auth_settings.REFRESH_SECURE,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
domain=auth_settings.COOKIE_DOMAIN,
)
response.set_cookie(
"access_token_lf",
@ -54,6 +55,7 @@ async def login_to_get_access_token(
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
domain=auth_settings.COOKIE_DOMAIN,
)
variable_service.initialize_user_variables(user.id, db)
# Create default folder for user if it doesn't exist
@ -71,8 +73,7 @@ async def login_to_get_access_token(
async def auto_login(
response: Response,
db: Session = Depends(get_session),
settings_service=Depends(get_settings_service),
variable_service: VariableService = Depends(get_variable_service),
settings_service=Depends(get_settings_service)
):
auth_settings = settings_service.auth_settings
if settings_service.auth_settings.AUTO_LOGIN:
@ -84,9 +85,9 @@ async def auto_login(
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=None, # Set to None to make it a session cookie
domain=auth_settings.COOKIE_DOMAIN,
)
variable_service.initialize_user_variables(user_id, db)
create_default_folder_if_it_doesnt_exist(db, user_id)
return tokens
raise HTTPException(
@ -117,6 +118,7 @@ async def refresh_token(
samesite=auth_settings.REFRESH_SAME_SITE,
secure=auth_settings.REFRESH_SECURE,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
domain=auth_settings.COOKIE_DOMAIN,
)
response.set_cookie(
"access_token_lf",
@ -125,6 +127,7 @@ async def refresh_token(
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
domain=auth_settings.COOKIE_DOMAIN,
)
return tokens
else:

View file

@ -1,9 +1,10 @@
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from langflow.services.deps import get_monitor_service
from langflow.services.monitor.schema import (
MessageModelRequest,
MessageModelResponse,
TransactionModelResponse,
VertexBuildMapModel,
@ -66,6 +67,44 @@ async def get_messages(
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/messages", status_code=204)
async def delete_messages(
message_ids: List[int],
monitor_service: MonitorService = Depends(get_monitor_service),
):
try:
monitor_service.delete_messages(message_ids=message_ids)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/messages/{message_id}", response_model=MessageModelResponse)
async def update_message(
message_id: str,
message: MessageModelRequest,
monitor_service: MonitorService = Depends(get_monitor_service),
):
try:
message_dict = message.model_dump(exclude_none=True)
message_dict.pop("index", None)
monitor_service.update_message(message_id=message_id, **message_dict)
return MessageModelResponse(index=message_id, **message_dict)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/messages/session/{session_id}", status_code=204)
async def delete_messages_session(
session_id: str,
monitor_service: MonitorService = Depends(get_monitor_service),
):
try:
monitor_service.delete_messages_session(session_id=session_id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/transactions", response_model=List[TransactionModelResponse])
async def get_transactions(
source: Optional[str] = Query(None),

View file

@ -256,6 +256,7 @@ class ResultDataResponse(BaseModel):
artifacts: Optional[Any] = Field(default_factory=dict)
timedelta: Optional[float] = None
duration: Optional[str] = None
used_frozen_result: Optional[bool] = False
class VertexBuildResponse(BaseModel):
@ -324,3 +325,7 @@ class FlowDataRequest(BaseModel):
nodes: List[dict]
edges: List[dict]
viewport: Optional[dict] = None
class ConfigResponse(BaseModel):
frontend_timeout: int

View file

@ -54,7 +54,7 @@ def check_if_store_is_enabled(
settings_service=Depends(get_settings_service),
):
return {
"enabled": settings_service.settings.STORE,
"enabled": settings_service.settings.store,
}

View file

@ -0,0 +1,169 @@
"""
This file contains a fix for the implementation of the `uncurl` library, which is available at https://github.com/spulec/uncurl.git.
The `uncurl` library provides a way to parse and convert cURL commands into Python requests. However, there are some issues with the original implementation that this file aims to fix.
The `parse_context` function in this file takes a cURL command as input and returns a `ParsedContext` object, which contains the parsed information from the cURL command, such as the HTTP method, URL, headers, cookies, etc.
The `normalize_newlines` function is a helper function that replaces the line continuation character ("\") followed by a newline with a space.
"""
import re
import shlex
from collections import OrderedDict, namedtuple
from http.cookies import SimpleCookie
ParsedArgs = namedtuple(
"ParsedContext",
[
"command",
"url",
"data",
"data_binary",
"method",
"headers",
"compressed",
"insecure",
"user",
"include",
"silent",
"proxy",
"proxy_user",
"cookies",
],
)
ParsedContext = namedtuple("ParsedContext", ["method", "url", "data", "headers", "cookies", "verify", "auth", "proxy"])
def normalize_newlines(multiline_text):
return multiline_text.replace(" \\\n", " ")
def parse_curl_command(curl_command):
tokens = shlex.split(normalize_newlines(curl_command))
tokens = [token for token in tokens if token and token != " "]
if "curl" not in tokens[0]:
raise ValueError("Invalid curl command")
args_template = {
"command": None,
"url": None,
"data": None,
"data_binary": None,
"method": "get",
"headers": [],
"compressed": False,
"insecure": False,
"user": (),
"include": False,
"silent": False,
"proxy": None,
"proxy_user": None,
"cookies": {},
}
args = args_template.copy()
i = 0
while i < len(tokens):
token = tokens[i]
if token == "-X":
i += 1
args["method"] = tokens[i].lower()
elif token in ("-d", "--data"):
i += 1
args["data"] = tokens[i]
args["method"] = "post"
elif token in ("-b", "--data-binary", "--data-raw"):
i += 1
args["data_binary"] = tokens[i]
args["method"] = "post"
elif token in ("-H", "--header"):
i += 1
args["headers"].append(tokens[i])
elif token == "--compressed":
args["compressed"] = True
elif token in ("-k", "--insecure"):
args["insecure"] = True
elif token in ("-u", "--user"):
i += 1
args["user"] = tuple(tokens[i].split(":"))
elif token in ("-I", "--include"):
args["include"] = True
elif token in ("-s", "--silent"):
args["silent"] = True
elif token in ("-x", "--proxy"):
i += 1
args["proxy"] = tokens[i]
elif token in ("-U", "--proxy-user"):
i += 1
args["proxy_user"] = tokens[i]
elif not token.startswith("-"):
if args["command"] is None:
args["command"] = token
else:
args["url"] = token
i += 1
return ParsedArgs(**args)
def parse_context(curl_command):
method = "get"
parsed_args: ParsedArgs = parse_curl_command(curl_command)
post_data = parsed_args.data or parsed_args.data_binary
if post_data:
method = "post"
if parsed_args.method:
method = parsed_args.method.lower()
cookie_dict = OrderedDict()
quoted_headers = OrderedDict()
for curl_header in parsed_args.headers:
if curl_header.startswith(":"):
occurrence = [m.start() for m in re.finditer(":", curl_header)]
header_key, header_value = curl_header[: occurrence[1]], curl_header[occurrence[1] + 1 :]
else:
header_key, header_value = curl_header.split(":", 1)
if header_key.lower().strip("$") == "cookie":
cookie = SimpleCookie(bytes(header_value, "ascii").decode("unicode-escape"))
for key in cookie:
cookie_dict[key] = cookie[key].value
else:
quoted_headers[header_key] = header_value.strip()
# add auth
user = parsed_args.user
if parsed_args.user:
user = tuple(user.split(":"))
# add proxy and its authentication if it's available.
proxies = parsed_args.proxy
# proxy_auth = parsed_args.proxy_user
if parsed_args.proxy and parsed_args.proxy_user:
proxies = {
"http": "http://{}@{}/".format(parsed_args.proxy_user, parsed_args.proxy),
"https": "http://{}@{}/".format(parsed_args.proxy_user, parsed_args.proxy),
}
elif parsed_args.proxy:
proxies = {
"http": "http://{}/".format(parsed_args.proxy),
"https": "http://{}/".format(parsed_args.proxy),
}
return ParsedContext(
method=method,
url=parsed_args.url,
data=post_data,
headers=quoted_headers,
cookies=cookie_dict,
verify=parsed_args.insecure,
auth=user,
proxy=proxies,
)

View file

@ -3,7 +3,7 @@ import xml.etree.ElementTree as ET
from concurrent import futures
from pathlib import Path
from typing import Callable, List, Optional, Text
import chardet
import yaml
from langflow.schema.schema import Record
@ -96,7 +96,12 @@ def retrieve_file_paths(
def read_text_file(file_path: str) -> str:
with open(file_path, "r") as f:
with open(file_path, "rb") as f:
raw_data = f.read()
result = chardet.detect(raw_data)
encoding = result["encoding"]
with open(file_path, "r", encoding=encoding) as f:
return f.read()

View file

@ -0,0 +1,67 @@
from typing import List
from langflow.graph.schema import ResultData, RunOutputs
from langflow.schema.schema import Record
def build_records_from_run_outputs(run_outputs: RunOutputs) -> List[Record]:
"""
Build a list of records from the given RunOutputs.
Args:
run_outputs (RunOutputs): The RunOutputs object containing the output data.
Returns:
List[Record]: A list of records built from the RunOutputs.
"""
if not run_outputs:
return []
records = []
for result_data in run_outputs.outputs:
if result_data:
records.extend(build_records_from_result_data(result_data))
return records
def build_records_from_result_data(result_data: ResultData, get_final_results_only: bool = True) -> List[Record]:
"""
Build a list of records from the given ResultData.
Args:
result_data (ResultData): The ResultData object containing the result data.
get_final_results_only (bool, optional): Whether to include only final results. Defaults to True.
Returns:
List[Record]: A list of records built from the ResultData.
"""
messages = result_data.messages
if not messages:
return []
records = []
for message in messages:
message_dict = message if isinstance(message, dict) else message.model_dump()
if get_final_results_only:
result_data_dict = result_data.model_dump()
results = result_data_dict.get("results", {})
inner_result = results.get("result", {})
record = Record(data={"result": inner_result, "message": message_dict}, text_key="result")
records.append(record)
return records
def format_flow_output_records(records: List[Record]) -> str:
"""
Format the flow output records into a string.
Args:
records (List[Record]): The list of records to format.
Returns:
str: The formatted flow output records.
"""
result = "Flow run output:\n"
results = "\n".join([record.result for record in records if record.data["message"]])
return result + results

View file

@ -1,9 +1,9 @@
from typing import Optional, Union
from langflow.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.interface.custom.custom_component import CustomComponent
from langflow.memory import store_message
from langflow.schema import Record

View file

@ -1,8 +1,8 @@
from typing import Optional
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema.schema import Record

View file

@ -1,6 +1,6 @@
from typing import Optional
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema.schema import Record

View file

@ -1,10 +1,10 @@
from fastapi import HTTPException
from langchain.prompts import PromptTemplate
from loguru import logger
from langflow.api.v1.base import INVALID_NAMES, check_input_variables
from langflow.interface.utils import extract_input_variables_from_prompt
from langflow.template.field.prompt import DefaultPromptField
from langchain_core.prompts import PromptTemplate
def validate_prompt(prompt_template: str, silent_errors: bool = False) -> list[str]:

View file

@ -0,0 +1,117 @@
from typing import Any, List, Optional, Type
from asyncer import syncify
from langchain.tools import BaseTool
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import ToolException
from pydantic.v1 import BaseModel
from langflow.base.flow_processing.utils import build_records_from_result_data, format_flow_output_records
from langflow.graph.graph.base import Graph
from langflow.graph.vertex.base import Vertex
from langflow.helpers.flow import build_schema_from_inputs, get_arg_names, get_flow_inputs, run_flow
class FlowTool(BaseTool):
name: str
description: str
graph: Optional[Graph] = None
flow_id: Optional[str] = None
user_id: Optional[str] = None
inputs: List["Vertex"] = []
get_final_results_only: bool = True
@property
def args(self) -> dict:
schema = self.get_input_schema()
return schema.schema()["properties"]
def get_input_schema(self, config: Optional[RunnableConfig] = None) -> Type[BaseModel]:
"""The tool's input schema."""
if self.args_schema is not None:
return self.args_schema
elif self.graph is not None:
return build_schema_from_inputs(self.name, get_flow_inputs(self.graph))
else:
raise ToolException("No input schema available.")
def _run(
self,
*args: Any,
**kwargs: Any,
) -> str:
"""Use the tool."""
args_names = get_arg_names(self.inputs)
if len(args_names) == len(args):
kwargs = {arg["arg_name"]: arg_value for arg, arg_value in zip(args_names, args)}
elif len(args_names) != len(args) and len(args) != 0:
raise ToolException(
"Number of arguments does not match the number of inputs. Pass keyword arguments instead."
)
tweaks = {arg["component_name"]: kwargs[arg["arg_name"]] for arg in args_names}
run_outputs = syncify(run_flow, raise_sync_error=False)(
tweaks={key: {"input_value": value} for key, value in tweaks.items()},
flow_id=self.flow_id,
user_id=self.user_id,
)
if not run_outputs:
return "No output"
run_output = run_outputs[0]
records = []
if run_output is not None:
for output in run_output.outputs:
if output:
records.extend(
build_records_from_result_data(output, get_final_results_only=self.get_final_results_only)
)
return format_flow_output_records(records)
def validate_inputs(self, args_names: List[dict[str, str]], args: Any, kwargs: Any):
"""Validate the inputs."""
if len(args) > 0 and len(args) != len(args_names):
raise ToolException(
"Number of positional arguments does not match the number of inputs. Pass keyword arguments instead."
)
if len(args) == len(args_names):
kwargs = {arg_name["arg_name"]: arg_value for arg_name, arg_value in zip(args_names, args)}
missing_args = [arg["arg_name"] for arg in args_names if arg["arg_name"] not in kwargs]
if missing_args:
raise ToolException(f"Missing required arguments: {', '.join(missing_args)}")
return kwargs
def build_tweaks_dict(self, args, kwargs):
args_names = get_arg_names(self.inputs)
kwargs = self.validate_inputs(args_names=args_names, args=args, kwargs=kwargs)
tweaks = {arg["component_name"]: kwargs[arg["arg_name"]] for arg in args_names}
return tweaks
async def _arun(
self,
*args: Any,
**kwargs: Any,
) -> str:
"""Use the tool asynchronously."""
tweaks = self.build_tweaks_dict(args, kwargs)
run_outputs = await run_flow(
tweaks={key: {"input_value": value} for key, value in tweaks.items()},
flow_id=self.flow_id,
user_id=self.user_id,
)
if not run_outputs:
return "No output"
run_output = run_outputs[0]
records = []
if run_output is not None:
for output in run_output.outputs:
if output:
records.extend(
build_records_from_result_data(output, get_final_results_only=self.get_final_results_only)
)
return format_flow_output_records(records)

View file

@ -1,52 +0,0 @@
from typing import Callable, List, Optional, Union
from langchain.agents import AgentExecutor, AgentType, initialize_agent, types
from langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool
from langflow.interface.custom.custom_component import CustomComponent
class AgentInitializerComponent(CustomComponent):
display_name: str = "Agent Initializer"
description: str = "Initialize a Langchain Agent."
documentation: str = "https://python.langchain.com/docs/modules/agents/agent_types/"
def build_config(self):
agents = list(types.AGENT_TO_CLASS.keys())
# field_type and required are optional
return {
"agent": {"options": agents, "value": agents[0], "display_name": "Agent Type"},
"max_iterations": {"display_name": "Max Iterations", "value": 10},
"memory": {"display_name": "Memory"},
"tools": {"display_name": "Tools"},
"llm": {"display_name": "Language Model"},
"code": {"advanced": True},
}
def build(
self,
agent: str,
llm: BaseLanguageModel,
tools: List[Tool],
max_iterations: int,
memory: Optional[BaseChatMemory] = None,
) -> Union[AgentExecutor, Callable]:
agent = AgentType(agent)
if memory:
return initialize_agent(
tools=tools,
llm=llm,
agent=agent,
memory=memory,
return_intermediate_steps=True,
handle_parsing_errors=True,
max_iterations=max_iterations,
)
return initialize_agent(
tools=tools,
llm=llm,
agent=agent,
return_intermediate_steps=True,
handle_parsing_errors=True,
max_iterations=max_iterations,
)

View file

@ -1,10 +1,9 @@
from langchain.agents import AgentExecutor, create_json_agent
from langchain.agents import AgentExecutor
from langchain_community.agent_toolkits import create_json_agent
from langchain_community.agent_toolkits.json.toolkit import JsonToolkit
from langflow.field_typing import (
BaseLanguageModel,
)
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel
class JsonAgentComponent(CustomComponent):

View file

@ -1,102 +0,0 @@
from typing import List, Optional
from langchain.agents.agent import AgentExecutor
from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import _get_default_system_message
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
from langchain.memory.token_buffer import ConversationTokenBufferMemory
from langchain.prompts import SystemMessagePromptTemplate
from langchain.prompts.chat import MessagesPlaceholder
from langchain.schema.memory import BaseMemory
from langchain.tools import Tool
from langchain_openai import ChatOpenAI
from langflow.field_typing.range_spec import RangeSpec
from langflow.interface.custom.custom_component import CustomComponent
from pydantic.v1 import SecretStr
class ConversationalAgent(CustomComponent):
display_name: str = "OpenAI Conversational Agent"
description: str = "Conversational Agent that can use OpenAI's function calling API"
icon = "OpenAI"
def build_config(self):
openai_function_models = [
"gpt-4-turbo-preview",
"gpt-4-0125-preview",
"gpt-4-1106-preview",
"gpt-4-vision-preview",
"gpt-3.5-turbo-0125",
"gpt-3.5-turbo-1106",
]
return {
"tools": {"display_name": "Tools"},
"memory": {"display_name": "Memory"},
"system_message": {"display_name": "System Message"},
"max_token_limit": {"display_name": "Max Token Limit"},
"model_name": {
"display_name": "Model Name",
"options": openai_function_models,
"value": openai_function_models[0],
},
"code": {"show": False},
"temperature": {
"display_name": "Temperature",
"value": 0.2,
"rangeSpec": RangeSpec(min=0, max=2, step=0.1),
},
}
def build(
self,
model_name: str,
openai_api_key: str,
tools: List[Tool],
openai_api_base: Optional[str] = None,
memory: Optional[BaseMemory] = None,
system_message: Optional[SystemMessagePromptTemplate] = None,
max_token_limit: int = 2000,
temperature: float = 0.9,
) -> AgentExecutor:
if openai_api_key:
api_key = SecretStr(openai_api_key)
else:
api_key = None
llm = ChatOpenAI(
model=model_name,
api_key=api_key,
base_url=openai_api_base,
max_tokens=max_token_limit,
temperature=temperature,
)
if not memory:
memory_key = "chat_history"
memory = ConversationTokenBufferMemory(
memory_key=memory_key,
return_messages=True,
output_key="output",
llm=llm,
max_token_limit=max_token_limit,
)
else:
memory_key = memory.memory_key # type: ignore
_system_message = system_message or _get_default_system_message()
prompt = OpenAIFunctionsAgent.create_prompt(
system_message=_system_message, # type: ignore
extra_prompt_messages=[MessagesPlaceholder(variable_name=memory_key)],
)
agent = OpenAIFunctionsAgent(
llm=llm,
tools=tools,
prompt=prompt, # type: ignore
)
return AgentExecutor(
agent=agent,
tools=tools, # type: ignore
memory=memory,
verbose=True,
return_intermediate_steps=True,
handle_parsing_errors=True,
)

View file

@ -1,12 +1,12 @@
from typing import Callable, Union
from langchain.agents import AgentExecutor
from langchain_community.utilities import SQLDatabase
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from langchain_community.agent_toolkits.sql.base import create_sql_agent
from langchain_community.utilities import SQLDatabase
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel
from langflow.interface.custom.custom_component import CustomComponent
class SQLAgentComponent(CustomComponent):

View file

@ -3,8 +3,8 @@ from typing import Callable, Union
from langchain.agents import AgentExecutor, create_vectorstore_agent
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreToolkit
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel
from langflow.interface.custom.custom_component import CustomComponent
class VectorStoreAgentComponent(CustomComponent):

View file

@ -4,7 +4,7 @@ from langchain.agents import create_vectorstore_router_agent
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreRouterToolkit
from langchain_core.language_models.base import BaseLanguageModel
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class VectorStoreRouterAgentComponent(CustomComponent):

View file

@ -1,17 +1,13 @@
from .AgentInitializer import AgentInitializerComponent
from .CSVAgent import CSVAgentComponent
from .JsonAgent import JsonAgentComponent
from .OpenAIConversationalAgent import ConversationalAgent
from .SQLAgent import SQLAgentComponent
from .VectorStoreAgent import VectorStoreAgentComponent
from .VectorStoreRouterAgent import VectorStoreRouterAgentComponent
from .XMLAgent import XMLAgentComponent
__all__ = [
"AgentInitializerComponent",
"CSVAgentComponent",
"JsonAgentComponent",
"ConversationalAgent",
"SQLAgentComponent",
"VectorStoreAgentComponent",
"VectorStoreRouterAgentComponent",

View file

@ -2,8 +2,8 @@ from typing import Optional
from langchain.chains import ConversationChain
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
from langflow.interface.custom.custom_component import CustomComponent
class ConversationChainComponent(CustomComponent):

View file

@ -1,11 +1,11 @@
from typing import Optional
from langchain.chains.llm import LLMChain
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
from langflow.interface.custom.custom_component import CustomComponent
from langchain_core.prompts import PromptTemplate
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
class LLMChainComponent(CustomComponent):
display_name = "LLMChain"

View file

@ -1,7 +1,7 @@
from langchain.chains import LLMCheckerChain
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, Text
from langflow.interface.custom.custom_component import CustomComponent
class LLMCheckerChainComponent(CustomComponent):

View file

@ -2,8 +2,8 @@ from typing import Optional
from langchain.chains import LLMChain, LLMMathChain
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
from langflow.interface.custom.custom_component import CustomComponent
class LLMMathChainComponent(CustomComponent):

View file

@ -3,8 +3,8 @@ from typing import Optional
from langchain.chains.retrieval_qa.base import RetrievalQA
from langchain_core.documents import Document
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema.schema import Record

View file

@ -3,8 +3,8 @@ from typing import Optional
from langchain.chains import RetrievalQAWithSourcesChain
from langchain_core.documents import Document
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
from langflow.interface.custom.custom_component import CustomComponent
class RetrievalQAWithSourcesChainComponent(CustomComponent):

View file

@ -5,8 +5,8 @@ from langchain_community.utilities.sql_database import SQLDatabase
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import Runnable
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel, Text
from langflow.interface.custom.custom_component import CustomComponent
class SQLGeneratorComponent(CustomComponent):

View file

@ -1,10 +1,15 @@
import asyncio
import json
from typing import List, Optional
from typing import Any, List, Optional
import httpx
from langflow.interface.custom.custom_component import CustomComponent
from loguru import logger
from langflow.base.curl.parse import parse_context
from langflow.custom import CustomComponent
from langflow.field_typing import NestedDict
from langflow.schema import Record
from langflow.schema.dotdict import dotdict
class APIRequest(CustomComponent):
@ -16,10 +21,15 @@ class APIRequest(CustomComponent):
field_config = {
"urls": {"display_name": "URLs", "info": "URLs to make requests to."},
"curl": {
"display_name": "Curl",
"info": "Paste a curl command to populate the fields.",
"refresh_button": True,
"refresh_button_text": "",
},
"method": {
"display_name": "Method",
"info": "The HTTP method to use.",
"field_type": "str",
"options": ["GET", "POST", "PATCH", "PUT"],
"value": "GET",
},
@ -35,12 +45,33 @@ class APIRequest(CustomComponent):
},
"timeout": {
"display_name": "Timeout",
"field_type": "int",
"info": "The timeout to use for the request.",
"value": 5,
},
}
def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:
try:
parsed = parse_context(curl)
build_config["urls"]["value"] = [parsed.url]
build_config["method"]["value"] = parsed.method.upper()
build_config["headers"]["value"] = dict(parsed.headers)
try:
json_data = json.loads(parsed.data)
build_config["body"]["value"] = json_data
except json.JSONDecodeError as e:
print(e)
except Exception as exc:
logger.error(f"Error parsing curl: {exc}")
raise ValueError(f"Error parsing curl: {exc}")
return build_config
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
if field_name == "curl" and field_value is not None:
build_config = self.parse_curl(field_value, build_config)
return build_config
async def make_request(
self,
client: httpx.AsyncClient,
@ -93,21 +124,25 @@ class APIRequest(CustomComponent):
self,
method: str,
urls: List[str],
headers: Optional[Record] = None,
body: Optional[Record] = None,
curl: Optional[str] = None,
headers: Optional[NestedDict] = {},
body: Optional[NestedDict] = {},
timeout: int = 5,
) -> List[Record]:
if headers is None:
headers_dict = {}
else:
elif isinstance(headers, Record):
headers_dict = headers.data
else:
headers_dict = headers
bodies = []
if body:
if isinstance(body, list):
bodies = [b.data for b in body]
if not isinstance(body, list):
bodies = [body]
else:
bodies = [body.data]
bodies = body
bodies = [b.data if isinstance(b, Record) else b for b in bodies] # type: ignore
if len(urls) != len(bodies):
# add bodies with None

View file

@ -1,7 +1,7 @@
from typing import Any, Dict, List, Optional
from langflow.base.data.utils import parallel_load_records, parse_text_file_to_record, retrieve_file_paths
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -2,7 +2,7 @@ from pathlib import Path
from typing import Any, Dict
from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -2,7 +2,7 @@ from typing import Any, Dict
from langchain_community.document_loaders.web_base import WebBaseLoader
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -0,0 +1,39 @@
import json
import uuid
from typing import Any, Optional
from langflow.custom import CustomComponent
from langflow.schema.dotdict import dotdict
from langflow.schema.schema import Record
class WebhookComponent(CustomComponent):
display_name = "Webhook Input"
description = "Defines a webhook input for the flow."
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
if field_name == "webhook_id":
build_config["webhook_id"]["value"] = uuid.uuid4().hex
return build_config
def build_config(self):
return {
"data": {
"display_name": "Data",
"info": "Use this field to quickly test the webhook component by providing a JSON payload.",
"multiline": True,
}
}
def build(self, data: Optional[str] = "") -> Record:
message = ""
try:
body = json.loads(data or "{}")
except json.JSONDecodeError:
body = {"payload": data}
message = f"Invalid JSON payload. Please check the format.\n\n{data}"
record = Record(data=body)
if not message:
message = json.dumps(body, indent=2)
self.status = message
return record

View file

@ -1,7 +1,8 @@
from .APIRequest import APIRequest
from .Directory import DirectoryComponent
from .File import FileComponent
from .Webhook import WebhookComponent
from .URL import URLComponent
__all__ = ["APIRequest", "DirectoryComponent", "FileComponent", "URLComponent"]
__all__ = ["APIRequest", "DirectoryComponent", "FileComponent", "URLComponent", "WebhookComponent"]

View file

@ -1,9 +1,9 @@
from typing import Optional
from langchain.embeddings.base import Embeddings
from langchain_community.embeddings import BedrockEmbeddings
from langchain_core.embeddings import Embeddings
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class AmazonBedrockEmeddingsComponent(CustomComponent):

View file

@ -1,7 +1,8 @@
from langchain.embeddings.base import Embeddings
from langchain_community.embeddings import AzureOpenAIEmbeddings
from langchain_core.embeddings import Embeddings
from langchain_openai import AzureOpenAIEmbeddings
from pydantic.v1 import SecretStr
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class AzureOpenAIEmbeddingsComponent(CustomComponent):
@ -52,12 +53,16 @@ class AzureOpenAIEmbeddingsComponent(CustomComponent):
api_version: str,
api_key: str,
) -> Embeddings:
if api_key:
azure_api_key = SecretStr(api_key)
else:
azure_api_key = None
try:
embeddings = AzureOpenAIEmbeddings(
azure_endpoint=azure_endpoint,
azure_deployment=azure_deployment,
api_version=api_version,
api_key=api_key,
api_key=azure_api_key,
)
except Exception as e:

View file

@ -2,7 +2,7 @@ from typing import Dict, Optional
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class HuggingFaceEmbeddingsComponent(CustomComponent):

View file

@ -3,7 +3,7 @@ from typing import Dict, Optional
from langchain_community.embeddings.huggingface import HuggingFaceInferenceAPIEmbeddings
from pydantic.v1.types import SecretStr
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class HuggingFaceInferenceAPIEmbeddingsComponent(CustomComponent):

View file

@ -1,7 +1,7 @@
from langchain_mistralai.embeddings import MistralAIEmbeddings
from pydantic.v1 import SecretStr
from langchain_mistralai.embeddings import MistralAIEmbeddings
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings

View file

@ -1,9 +1,9 @@
from typing import Optional
from langchain.embeddings.base import Embeddings
from langchain_community.embeddings import OllamaEmbeddings
from langchain_core.embeddings import Embeddings
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class OllamaEmbeddingsComponent(CustomComponent):

View file

@ -1,10 +1,10 @@
from typing import Any, Dict, List, Optional
from typing import Dict, List, Optional
from langchain_openai.embeddings.base import OpenAIEmbeddings
from pydantic.v1 import SecretStr
from langflow.custom import CustomComponent
from langflow.field_typing import Embeddings, NestedDict
from langflow.interface.custom.custom_component import CustomComponent
class OpenAIEmbeddingsComponent(CustomComponent):
@ -94,7 +94,6 @@ class OpenAIEmbeddingsComponent(CustomComponent):
allowed_special: List[str] = [],
disallowed_special: List[str] = ["all"],
chunk_size: int = 1000,
client: Optional[Any] = None,
deployment: str = "text-embedding-ada-002",
embedding_ctx_length: int = 8191,
max_retries: int = 6,
@ -126,7 +125,6 @@ class OpenAIEmbeddingsComponent(CustomComponent):
allowed_special=set(allowed_special),
disallowed_special="all",
chunk_size=chunk_size,
client=client,
deployment=deployment,
embedding_ctx_length=embedding_ctx_length,
max_retries=max_retries,

View file

@ -2,7 +2,7 @@ from typing import List, Optional
from langchain_google_vertexai import VertexAIEmbeddings
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class VertexAIEmbeddingsComponent(CustomComponent):

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.memory import delete_messages, get_messages

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,14 +1,14 @@
from typing import Any, List, Optional
from asyncer import syncify
from langchain_core.tools import StructuredTool
from loguru import logger
from langflow.base.tools.flow_tool import FlowTool
from langflow.custom 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.helpers.flow import get_flow_inputs
from langflow.schema.dotdict import dotdict
from langflow.schema.schema import Record
from loguru import logger
class FlowToolComponent(CustomComponent):
@ -68,18 +68,20 @@ class FlowToolComponent(CustomComponent):
}
async def build(self, flow_name: str, name: str, description: str, return_direct: bool = False) -> Tool:
FlowTool.update_forward_refs()
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(
func=syncify(dynamic_flow_function, raise_sync_error=False), # type: ignore
coroutine=dynamic_flow_function,
inputs = get_flow_inputs(graph)
tool = FlowTool(
name=name,
description=description,
graph=graph,
return_direct=return_direct,
args_schema=schema,
inputs=inputs,
flow_id=str(flow_record.id),
user_id=str(self._user_id),
)
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()])

View file

@ -1,6 +1,6 @@
from typing import List
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,6 +1,6 @@
from typing import Optional
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,7 +1,8 @@
from typing import Union
from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.schema import Record
class PassComponent(CustomComponent):

View file

@ -1,8 +1,8 @@
from typing import Callable
from langflow.custom import CustomComponent
from langflow.custom.utils import get_function
from langflow.field_typing import Code
from langflow.interface.custom.custom_component import CustomComponent
from langflow.interface.custom.utils import get_function
class PythonFunctionComponent(CustomComponent):

View file

@ -1,8 +1,9 @@
from typing import Any, List, Optional
from langflow.base.flow_processing.utils import build_records_from_run_outputs
from langflow.custom import CustomComponent
from langflow.field_typing import NestedDict, Text
from langflow.graph.schema import ResultData
from langflow.graph.schema import RunOutputs
from langflow.schema import Record, dotdict
@ -39,28 +40,17 @@ class RunFlowComponent(CustomComponent):
},
}
def build_records_from_result_data(self, result_data: ResultData) -> List[Record]:
messages = result_data.messages
if not messages:
return []
records = []
for message in messages:
message_dict = message if isinstance(message, dict) else message.model_dump()
record = Record(text=message_dict.get("text", ""), data={"result": result_data})
records.append(record)
return records
async def build(self, input_value: Text, flow_name: str, tweaks: NestedDict) -> List[Record]:
results: List[Optional[ResultData]] = await self.run_flow(
results: List[Optional[RunOutputs]] = await self.run_flow(
inputs={"input_value": input_value}, flow_name=flow_name, tweaks=tweaks
)
if isinstance(results, list):
records = []
for result in results:
if result:
records.extend(self.build_records_from_result_data(result))
records.extend(build_records_from_run_outputs(result))
else:
records = self.build_records_from_result_data(results)
records = build_records_from_run_outputs()(results)
self.status = records
return records

View file

@ -1,7 +1,7 @@
from langchain_core.runnables import Runnable
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.interface.custom.custom_component import CustomComponent
class RunnableExecComponent(CustomComponent):

View file

@ -1,8 +1,8 @@
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
from langchain_community.utilities import SQLDatabase
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.interface.custom.custom_component import CustomComponent
class SQLExecutorComponent(CustomComponent):

View file

@ -1,7 +1,7 @@
from typing import Optional
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record
from langflow.utils.util import unescape_string
@ -43,7 +43,7 @@ class SplitTextComponent(CustomComponent):
chunks = [chunk[:truncate_size] for chunk in chunks]
for chunk in chunks:
outputs.append(Record(text=chunk, data={"parent": text}))
outputs.append(Record(data={"parent": text, "text": chunk}))
self.status = outputs
return outputs

View file

@ -1,6 +1,6 @@
from typing import List, Optional
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.memory import get_messages, store_message
from langflow.schema import Record

View file

@ -2,9 +2,10 @@ from typing import Any, List, Optional
from loguru import logger
from langflow.base.flow_processing.utils import build_records_from_result_data
from langflow.custom import CustomComponent
from langflow.graph.graph.base import Graph
from langflow.graph.schema import ResultData, RunOutputs
from langflow.graph.schema import RunOutputs
from langflow.graph.vertex.base import Vertex
from langflow.helpers.flow import get_flow_inputs
from langflow.schema import Record
@ -92,21 +93,6 @@ class SubFlowComponent(CustomComponent):
},
}
def build_records_from_result_data(self, result_data: ResultData, get_final_results_only: bool) -> List[Record]:
messages = result_data.messages
if not messages:
return []
records = []
for message in messages:
message_dict = message if isinstance(message, dict) else message.model_dump()
if get_final_results_only:
result_data_dict = result_data.model_dump()
results = result_data_dict.get("results", {})
inner_result = results.get("result", {})
record = Record(data={"result": inner_result, "message": message_dict}, text_key="result")
records.append(record)
return records
async def build(self, flow_name: str, get_final_results_only: bool = True, **kwargs) -> List[Record]:
tweaks = {key: {"input_value": value} for key, value in kwargs.items()}
run_outputs: List[Optional[RunOutputs]] = await self.run_flow(
@ -121,7 +107,7 @@ class SubFlowComponent(CustomComponent):
if run_output is not None:
for output in run_output.outputs:
if output:
records.extend(self.build_records_from_result_data(output, get_final_results_only))
records.extend(build_records_from_result_data(output, get_final_results_only))
self.status = records
logger.debug(records)

View file

@ -1,8 +1,8 @@
from typing import Optional, Union
from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.schema import Record
class TextOperatorComponent(CustomComponent):

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.field_typing import Text

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.field_typing import Text

View file

@ -1,6 +1,6 @@
# from langflow.field_typing import Data
from langflow.custom import CustomComponent
from langflow.schema import Record
from langflow.interface.custom.custom_component import CustomComponent
class Component(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import List
from langchain_core.documents import Document
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,7 +1,7 @@
import uuid
from typing import Any, Optional
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class UUIDGeneratorComponent(CustomComponent):

View file

@ -1,6 +1,6 @@
from typing import List, Optional
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.memory import get_messages
from langflow.schema import Record

View file

@ -1,6 +1,6 @@
from langflow.custom import CustomComponent
from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.interface.custom.custom_component import CustomComponent
from langflow.schema import Record

View file

@ -1,4 +1,4 @@
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.schema import Record

View file

@ -1,7 +1,7 @@
from langchain_core.prompts import PromptTemplate
from langflow.custom import CustomComponent
from langflow.field_typing import Prompt, TemplateField, Text
from langflow.interface.custom.custom_component import CustomComponent
class PromptComponent(CustomComponent):

View file

@ -3,7 +3,7 @@
# We need to make sure this class is importable from the context where this code will be running.
from langchain_community.utilities.bing_search import BingSearchAPIWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class BingSearchAPIWrapperComponent(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import Callable, Union
from langchain_community.utilities.google_search import GoogleSearchAPIWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class GoogleSearchAPIWrapperComponent(CustomComponent):

View file

@ -4,7 +4,7 @@ from typing import Dict
# If this class does not exist, you would need to create it or import the appropriate class from another module
from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class GoogleSerperAPIWrapperComponent(CustomComponent):

View file

@ -13,7 +13,7 @@
from langchain_core.documents import Document
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.services.database.models.base import orjson_dumps

View file

@ -1,6 +1,6 @@
from langchain_experimental.sql.base import SQLDatabase
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class SQLDatabaseComponent(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import Dict, Optional
from langchain_community.utilities.searx_search import SearxSearchWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class SearxSearchWrapperComponent(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import Callable, Union
from langchain_community.utilities.serpapi import SerpAPIWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class SerpAPIWrapperComponent(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import Callable, Union
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
# Assuming WikipediaAPIWrapper is a class that needs to be imported.
# The import statement is not included as it is not provided in the JSON

View file

@ -2,7 +2,7 @@ from typing import Callable, Union
from langchain_community.utilities.wolfram_alpha import WolframAlphaAPIWrapper
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
# Since all the fields in the JSON have show=False, we will only create a basic component
# without any configurable fields.

View file

@ -51,9 +51,7 @@ class AstraDBMessageReaderComponent(BaseMemoryComponent):
Returns:
list[Record]: A list of Record objects representing the search results.
"""
memory: AstraDBChatMessageHistory = cast(
AstraDBChatMessageHistory, kwargs.get("memory")
)
memory: AstraDBChatMessageHistory = cast(AstraDBChatMessageHistory, kwargs.get("memory"))
if not memory:
raise ValueError("AstraDBChatMessageHistory instance is required.")
@ -72,9 +70,7 @@ class AstraDBMessageReaderComponent(BaseMemoryComponent):
namespace: Optional[str] = None,
) -> list[Record]:
try:
from langchain_community.chat_message_histories.astradb import (
AstraDBChatMessageHistory,
)
pass
except ImportError:
raise ImportError(
"Could not import langchain Astra DB integration package. "

View file

@ -5,7 +5,7 @@ from langflow.field_typing import Text
from langflow.schema.schema import Record
from langchain_core.messages import BaseMessage
from langchain_community.chat_message_histories.astradb import AstraDBChatMessageHistory
from langchain_astradb import AstraDBChatMessageHistory
class AstraDBMessageWriterComponent(BaseMemoryComponent):
@ -74,13 +74,15 @@ class AstraDBMessageWriterComponent(BaseMemoryComponent):
if memory is None:
raise ValueError("AstraDBChatMessageHistory instance is required.")
text_list = [BaseMessage(
content=text,
sender=sender,
sender_name=sender_name,
metadata=metadata,
session_id=session_id,
)]
text_list = [
BaseMessage(
content=text,
sender=sender,
sender_name=sender_name,
metadata=metadata,
session_id=session_id,
)
]
memory.add_messages(text_list)
@ -94,9 +96,7 @@ class AstraDBMessageWriterComponent(BaseMemoryComponent):
namespace: Optional[str] = None,
) -> Record:
try:
from langchain_community.chat_message_histories.astradb import (
AstraDBChatMessageHistory,
)
pass
except ImportError:
raise ImportError(
"Could not import langchain Astra DB integration package. "

View file

@ -116,19 +116,18 @@ class ZepMessageReaderComponent(BaseMemoryComponent):
url: Optional[Text] = None,
api_key: Optional[Text] = None,
query: Optional[Text] = None,
search_scope: SearchScope = SearchScope.messages,
search_type: SearchType = SearchType.similarity,
search_scope: str = SearchScope.messages,
search_type: str = SearchType.similarity,
limit: Optional[int] = None,
) -> list[Record]:
try:
from zep_python import ZepClient
from zep_python.langchain import ZepChatMessageHistory
# Monkeypatch API_BASE_PATH to
# avoid 404
# This is a workaround for the local Zep instance
# cloud Zep works with v2
import zep_python.zep_client
from zep_python import ZepClient
from zep_python.langchain import ZepChatMessageHistory
zep_python.zep_client.API_BASE_PATH = api_base_path
except ImportError:

View file

@ -1,8 +1,9 @@
from typing import Optional
from langflow.field_typing import BaseLanguageModel
from langchain_community.llms.bedrock import Bedrock
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel
class AmazonBedrockComponent(CustomComponent):

View file

@ -1,10 +1,10 @@
from typing import Optional
from langchain.llms.base import BaseLanguageModel
from langchain_anthropic import ChatAnthropic
from langchain_core.language_models import BaseLanguageModel
from pydantic.v1 import SecretStr
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class ChatAntropicSpecsComponent(CustomComponent):
@ -35,8 +35,8 @@ class ChatAntropicSpecsComponent(CustomComponent):
},
"max_tokens": {
"display_name": "Max Tokens",
"field_type": "int",
"value": 256,
"advanced": True,
"info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.",
},
"temperature": {
"display_name": "Temperature",

View file

@ -1,9 +1,10 @@
from typing import Optional
from langchain.llms.base import BaseLanguageModel
from langchain_community.chat_models.azure_openai import AzureChatOpenAI
from langchain_core.language_models import BaseLanguageModel
from langchain_openai import AzureChatOpenAI
from pydantic.v1 import SecretStr
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
class AzureChatOpenAISpecsComponent(CustomComponent):
@ -65,11 +66,8 @@ class AzureChatOpenAISpecsComponent(CustomComponent):
},
"max_tokens": {
"display_name": "Max Tokens",
"value": 1000,
"required": False,
"field_type": "int",
"advanced": True,
"info": "Maximum number of tokens to generate.",
"info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.",
},
"code": {"show": False},
}
@ -84,15 +82,19 @@ class AzureChatOpenAISpecsComponent(CustomComponent):
temperature: float = 0.7,
max_tokens: Optional[int] = 1000,
) -> BaseLanguageModel:
if api_key:
azure_api_key = SecretStr(api_key)
else:
azure_api_key = None
try:
llm = AzureChatOpenAI(
model=model,
azure_endpoint=azure_endpoint,
azure_deployment=azure_deployment,
api_version=api_version,
api_key=api_key,
api_key=azure_api_key,
temperature=temperature,
max_tokens=max_tokens,
max_tokens=max_tokens or None,
)
except Exception as e:
raise ValueError("Could not connect to AzureOpenAI API.") from e

View file

@ -1,10 +1,11 @@
from typing import Optional
from langchain_community.chat_models.baidu_qianfan_endpoint import QianfanChatEndpoint
from pydantic.v1 import SecretStr
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel
from langflow.interface.custom.custom_component import CustomComponent
class QianfanChatEndpointComponent(CustomComponent):

View file

@ -2,7 +2,7 @@ from typing import Optional
from langchain_community.llms.baidu_qianfan_endpoint import QianfanLLMEndpoint
from langflow.interface.custom.custom_component import CustomComponent
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel

View file

@ -46,9 +46,8 @@ class AnthropicLLM(CustomComponent):
},
"max_tokens": {
"display_name": "Max Tokens",
"field_type": "int",
"advanced": True,
"value": 256,
"info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.",
},
"temperature": {
"display_name": "Temperature",

View file

@ -1,8 +1,9 @@
from typing import Any, Dict, Optional
from langchain_community.chat_models.litellm import ChatLiteLLM, ChatLiteLLMException
from langflow.custom import CustomComponent
from langflow.field_typing import BaseLanguageModel
from langflow.interface.custom.custom_component import CustomComponent
class ChatLiteLLMComponent(CustomComponent):
@ -81,12 +82,9 @@ class ChatLiteLLMComponent(CustomComponent):
"default": 1,
},
"max_tokens": {
"display_name": "Max tokens",
"field_type": "int",
"advanced": False,
"required": False,
"default": 256,
"info": "The maximum number of tokens to generate for each chat completion.",
"display_name": "Max Tokens",
"advanced": True,
"info": "The maximum number of tokens to generate. Set to 0 for unlimited tokens.",
},
"max_retries": {
"display_name": "Max retries",

Some files were not shown because too many files have changed in this diff Show more