Add webhook endpoint, and other fixes (#1766)
* Update FastAPI app configuration in main.py * Add webhook endpoint * Add Observer model and related changes to database models * Update refresh button text in ParameterComponent * Update database models and main.py, and add webhook endpoint and utils for flow model * Add WebhookComponent to langflow data components * Update webhook component names in flow data and remove unused code in edge/base.py and graph/base.py * 🐛 (endpoints.py): fix an issue where the user_id was not being correctly assigned based on the api_key_user or flow object * 📝 (endpoints.py): Update documentation for the webhook_run_flow endpoint to provide clearer information on its purpose, arguments, and return value. Remove outdated request JSON structure and example request sections. Add information on the arguments and exceptions raised for better clarity. * chore: Remove unnecessary trailing commas in code files * feat: Add yield statement to get_lifespan function * feat: Update webhook columns revision ID * Refactor import statements for OpenAI conversational agent * refactor(constants.ts): remove unused tabsArray function and update function name to createTabsArray for clarity feat(constants.ts): add support for including webhook code in tabs array based on flow webhook property feat(index.tsx): update function calls to use new createTabsArray function and include webhook code in tabs array based on flow webhook property refactor(utils.ts): remove trailing commas in function parameters to improve code consistency and readability feat(utils.ts): add support for including webhook cURL code and tweaks code in tabs array creation for better code organization and presentation * chore: Update folder_id parameter type to str in API endpoints * feat: Handle invalid JSON payload in WebhookComponent The code changes in `Webhook.py` modify the `build` method of the `WebhookComponent` class. If an invalid JSON payload is provided, instead of raising a `ValueError`, the code now handles the exception by creating a default payload with the provided data. The commit also includes a message that describes the invalid JSON payload for better error tracking. Note: This commit message follows the convention used in the recent user commits. * refactor: Remove unnecessary code in main.py * refactor: Update Alembic revision IDs for webhook columns The Alembic revision IDs for adding webhook columns have been updated to reflect the latest changes. This ensures consistency and proper tracking of the database schema modifications. Note: The commit message has been generated based on the provided code changes and recent commits. * 🐛 (endpoints.py): Fix handling of flow_id_or_name variable to correctly identify flow ID or endpoint name for running a flow. Improve error handling for invalid UUID strings and flow not found scenarios. * refactor: Handle invalid JSON payload in WebhookComponent The code changes in `Webhook.py` modify the `build` method of the `WebhookComponent` class. If an invalid JSON payload is provided, instead of raising a `ValueError`, the code now handles the exception by creating a default payload with the provided data. * refactor: Update get_flow_by_id function to handle None flow_id parameter The get_flow_by_id function in utils.py has been updated to handle the case where the flow_id parameter is None. Previously, the function would raise an exception if flow_id was not provided. Now, if flow_id is None, the function returns None instead of querying the database. This change improves the robustness of the function and allows for more flexible usage. Note: The commit message has been generated based on the provided code changes and recent commits. * 📝 (endpoints.py): Add import statement for get_flow_by_id_or_endpoint_name function 📝 (flow.py): Add get_flow_by_id_or_endpoint_name function to retrieve a Flow object by ID or endpoint name * refactor: Update get_flow_by_id_or_endpoint_name function to handle None flow_id parameter * 🐛 (flow.py): replace ValueError with HTTPException to return a 404 status code when flow identifier is not found * refactor: Update error message for flow identifier not found The code changes in `test_endpoints.py` update the error message when a flow identifier is not found. Previously, the error message referenced the flow ID directly, but now it uses the more generic term "Flow identifier" to account for cases where the identifier could be an endpoint name. This change improves the clarity and flexibility of the error message. Note: This commit message has been generated based on the provided code changes and recent commits. * 🔧 (endpoints.py): remove unnecessary flow_id_or_name parameter from the simple_run_flow function call to improve code readability and maintainability
This commit is contained in:
parent
1f8d0eb1c0
commit
5e2f4b8f83
41 changed files with 468 additions and 295 deletions
|
|
@ -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 ###
|
||||
|
|
@ -3,7 +3,7 @@ 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
|
||||
|
||||
|
|
@ -22,11 +22,14 @@ from langflow.api.v1.schemas import (
|
|||
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.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
|
||||
|
|
@ -53,10 +56,75 @@ def get_all(
|
|||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
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 {})
|
||||
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)
|
||||
]
|
||||
# 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,
|
||||
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_or_name: str,
|
||||
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),
|
||||
|
|
@ -67,7 +135,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.
|
||||
|
|
@ -110,89 +178,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
|
||||
endpoint_name = None
|
||||
flow_id_str = None
|
||||
try:
|
||||
try:
|
||||
flow_id = UUID(flow_id_or_name)
|
||||
|
||||
except ValueError:
|
||||
endpoint_name = flow_id_or_name
|
||||
flow = db.exec(
|
||||
select(Flow).where(Flow.endpoint_name == endpoint_name).where(Flow.user_id == api_key_user.id)
|
||||
).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow with endpoint name {endpoint_name} not found")
|
||||
flow_id = flow.id
|
||||
|
||||
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 flow_id_str and 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 endpoint_name and f"Flow with endpoint name {endpoint_name} not found" in str(exc):
|
||||
logger.error(f"Flow with endpoint name {endpoint_name} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
elif session_id and 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)
|
||||
|
|
@ -202,6 +202,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)],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from langflow.api.v1.schemas import FlowListCreate, FlowListIds, 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
|
||||
|
|
@ -150,6 +151,8 @@ def update_flow(
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
|
||||
|
|
@ -88,7 +87,7 @@ def read_folders(
|
|||
def read_folder(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder_id: UUID,
|
||||
folder_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
|
|
@ -106,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),
|
||||
):
|
||||
|
|
@ -155,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:
|
||||
|
|
@ -177,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."""
|
||||
|
|
|
|||
39
src/backend/base/langflow/components/data/Webhook.py
Normal file
39
src/backend/base/langflow/components/data/Webhook.py
Normal 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
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from langflow.schema.schema import INPUT_FIELD_NAME, InputType
|
|||
from langflow.services.cache.utils import CacheMiss
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.deps import get_chat_service
|
||||
from langflow.services.monitor.utils import log_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.schema import ResultData
|
||||
|
|
@ -763,9 +764,11 @@ class Graph:
|
|||
next_runnable_vertices, top_level_vertices = await self.get_next_and_top_level_vertices(
|
||||
lock, set_cache_coro, vertex
|
||||
)
|
||||
log_transaction(vertex, status="success")
|
||||
return next_runnable_vertices, top_level_vertices, result_dict, params, valid, artifacts, vertex
|
||||
except Exception as exc:
|
||||
logger.exception(f"Error building vertex: {exc}")
|
||||
log_transaction(vertex, status="failure", error=str(exc))
|
||||
raise exc
|
||||
|
||||
async def get_next_and_top_level_vertices(
|
||||
|
|
|
|||
|
|
@ -696,7 +696,8 @@ class Vertex:
|
|||
|
||||
self._finalize_build()
|
||||
|
||||
return await self.get_requester_result(requester)
|
||||
result = await self.get_requester_result(requester)
|
||||
return result
|
||||
|
||||
async def get_requester_result(self, requester: Optional["Vertex"]):
|
||||
# If the requester is None, this means that
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple, Type, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from pydantic.v1 import BaseModel, Field, create_model
|
||||
from sqlmodel import select
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.graph.schema import RunOutputs
|
||||
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
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.deps import get_session, session_scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.graph.base import Graph
|
||||
|
|
@ -235,3 +236,22 @@ def get_arg_names(inputs: List["Vertex"]) -> List[dict[str, str]]:
|
|||
{"component_name": input_.display_name, "arg_name": input_.display_name.lower().replace(" ", "_")}
|
||||
for input_ in inputs
|
||||
]
|
||||
|
||||
|
||||
def get_flow_by_id_or_endpoint_name(
|
||||
flow_id_or_name: str, db: Session = Depends(get_session), user_id: Optional[UUID] = None
|
||||
) -> Flow:
|
||||
endpoint_name = None
|
||||
try:
|
||||
flow_id = UUID(flow_id_or_name)
|
||||
flow = db.get(Flow, flow_id)
|
||||
except ValueError:
|
||||
endpoint_name = flow_id_or_name
|
||||
stmt = select(Flow).where(Flow.name == endpoint_name)
|
||||
if user_id:
|
||||
stmt = stmt.where(Flow.user_id == user_id)
|
||||
flow = db.exec(stmt).first()
|
||||
if flow is None:
|
||||
raise HTTPException(status_code=404, detail=f"Flow identifier {flow_id_or_name} not found")
|
||||
|
||||
return flow
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ from rich import print as rprint
|
|||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from langflow.api import router
|
||||
from langflow.initial_setup.setup import create_or_update_starter_projects, initialize_super_user_if_needed, load_flows_from_directory
|
||||
from langflow.initial_setup.setup import (
|
||||
create_or_update_starter_projects,
|
||||
initialize_super_user_if_needed,
|
||||
load_flows_from_directory,
|
||||
)
|
||||
from langflow.interface.utils import setup_llm_caching
|
||||
from langflow.services.plugins.langfuse_plugin import LangfuseInstance
|
||||
from langflow.services.utils import initialize_services, teardown_services
|
||||
|
|
@ -33,20 +37,13 @@ class JavaScriptMIMETypeMiddleware(BaseHTTPMiddleware):
|
|||
return response
|
||||
|
||||
|
||||
def get_lifespan(fix_migration=False, socketio_server=None):
|
||||
try:
|
||||
from langflow.version import __version__ # type: ignore
|
||||
except ImportError:
|
||||
from importlib.metadata import version
|
||||
|
||||
__version__ = version("langflow-base")
|
||||
|
||||
def get_lifespan(fix_migration=False, socketio_server=None, version=None):
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
nest_asyncio.apply()
|
||||
# Startup message
|
||||
if __version__:
|
||||
rprint(f"[bold green]Starting Langflow v{__version__}...[/bold green]")
|
||||
if version:
|
||||
rprint(f"[bold green]Starting Langflow v{version}...[/bold green]")
|
||||
else:
|
||||
rprint("[bold green]Starting Langflow...[/bold green]")
|
||||
try:
|
||||
|
|
@ -70,11 +67,17 @@ def get_lifespan(fix_migration=False, socketio_server=None):
|
|||
|
||||
def create_app():
|
||||
"""Create the FastAPI app and include the router."""
|
||||
try:
|
||||
from langflow.version import __version__ # type: ignore
|
||||
except ImportError:
|
||||
from importlib.metadata import version
|
||||
|
||||
__version__ = version("langflow-base")
|
||||
|
||||
configure()
|
||||
socketio_server = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*", logger=True)
|
||||
lifespan = get_lifespan(socketio_server=socketio_server)
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
lifespan = get_lifespan(socketio_server=socketio_server, version=__version__)
|
||||
app = FastAPI(lifespan=lifespan, title="Langflow", version=__version__)
|
||||
origins = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class FlowBase(SQLModel):
|
|||
data: Optional[Dict] = Field(default=None, nullable=True)
|
||||
is_component: Optional[bool] = Field(default=False, nullable=True)
|
||||
updated_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=True)
|
||||
webhook: Optional[bool] = Field(default=False, nullable=True, description="Can be used on the webhook endpoint")
|
||||
folder_id: Optional[UUID] = Field(default=None, nullable=True)
|
||||
endpoint_name: Optional[str] = Field(default=None, nullable=True, index=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.services.deps import get_session
|
||||
|
||||
from .model import Flow
|
||||
|
||||
|
||||
def get_flow_by_id(session: Session = Depends(get_session), flow_id: Optional[str] = None) -> Flow | None:
|
||||
"""Get flow by id."""
|
||||
|
||||
if flow_id is None:
|
||||
raise ValueError("Flow id is required.")
|
||||
|
||||
return session.get(Flow, flow_id)
|
||||
|
||||
|
||||
def get_webhook_component_in_flow(flow_data: dict):
|
||||
"""Get webhook component in flow data."""
|
||||
|
||||
for node in flow_data.get("nodes", []):
|
||||
if "Webhook" in node.get("id"):
|
||||
return node
|
||||
return None
|
||||
|
||||
|
||||
def get_all_webhook_components_in_flow(flow_data: dict | None):
|
||||
"""Get all webhook components in flow data."""
|
||||
if not flow_data:
|
||||
return []
|
||||
return [node for node in flow_data.get("nodes", []) if "Webhook" in node.get("id")]
|
||||
|
|
@ -8,6 +8,7 @@ from langflow.services.deps import get_monitor_service
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.api.v1.schemas import ResultDataResponse
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
|
||||
|
||||
INDEX_KEY = "index"
|
||||
|
|
@ -165,3 +166,35 @@ async def log_vertex_build(
|
|||
monitor_service.add_row(table_name="vertex_builds", data=row)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error logging vertex build: {e}")
|
||||
|
||||
|
||||
def build_clean_params(target: "Vertex") -> dict:
|
||||
"""
|
||||
Cleans the parameters of the target vertex.
|
||||
"""
|
||||
# Removes all keys that the values aren't python types like str, int, bool, etc.
|
||||
params = {
|
||||
key: value for key, value in target.params.items() if isinstance(value, (str, int, bool, float, list, dict))
|
||||
}
|
||||
# if it is a list we need to check if the contents are python types
|
||||
for key, value in params.items():
|
||||
if isinstance(value, list):
|
||||
params[key] = [item for item in value if isinstance(item, (str, int, bool, float, list, dict))]
|
||||
return params
|
||||
|
||||
|
||||
def log_transaction(vertex: "Vertex", status, error=None):
|
||||
try:
|
||||
monitor_service = get_monitor_service()
|
||||
clean_params = build_clean_params(vertex)
|
||||
data = {
|
||||
"vertex_id": vertex.id,
|
||||
"inputs": clean_params,
|
||||
"output": str(vertex.result),
|
||||
"timestamp": monitor_service.get_timestamp(),
|
||||
"status": status,
|
||||
"error": error,
|
||||
}
|
||||
monitor_service.add_row(table_name="transactions", data=data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error logging transaction: {e}")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function FolderAccordionComponent({
|
|||
options,
|
||||
}: AccordionComponentType): JSX.Element {
|
||||
const [value, setValue] = useState(
|
||||
open.length === 0 ? "" : getOpenAccordion(),
|
||||
open.length === 0 ? "" : getOpenAccordion()
|
||||
);
|
||||
|
||||
function getOpenAccordion(): string {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { storeComponent } from "../../../../types/store";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||
import ShadTooltip from "../../../shadTooltipComponent";
|
||||
import { Card, CardHeader, CardTitle } from "../../../ui/card";
|
||||
|
||||
export default function DragCardComponent({ data }: { data: storeComponent }) {
|
||||
|
|
@ -11,7 +10,7 @@ export default function DragCardComponent({ data }: { data: storeComponent }) {
|
|||
draggable
|
||||
//TODO check color schema
|
||||
className={cn(
|
||||
"group relative flex flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
|
||||
"group relative flex flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
|
@ -23,7 +22,7 @@ export default function DragCardComponent({ data }: { data: storeComponent }) {
|
|||
"visible flex-shrink-0",
|
||||
data.is_component
|
||||
? "mx-0.5 h-6 w-6 text-component-icon"
|
||||
: "h-7 w-7 flex-shrink-0 text-flow-icon",
|
||||
: "h-7 w-7 flex-shrink-0 text-flow-icon"
|
||||
)}
|
||||
name={data.is_component ? "ToyBrick" : "Group"}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default function CodeAreaComponent({
|
|||
setOpen,
|
||||
}: CodeAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(
|
||||
typeof value == "string" ? value : JSON.stringify(value),
|
||||
typeof value == "string" ? value : JSON.stringify(value)
|
||||
);
|
||||
useEffect(() => {
|
||||
if (disabled && myValue !== "") {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default function Dropdown({
|
|||
? "dropdown-component-outline"
|
||||
: "dropdown-component-false-outline",
|
||||
"w-full justify-between font-normal",
|
||||
editNode ? "input-edit-node" : "py-2",
|
||||
editNode ? "input-edit-node" : "py-2"
|
||||
)}
|
||||
>
|
||||
<span data-testid={`value-dropdown-` + id}>
|
||||
|
|
@ -107,7 +107,7 @@ export default function Dropdown({
|
|||
name="Check"
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 text-primary",
|
||||
value === option ? "opacity-100" : "opacity-0",
|
||||
value === option ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const ForwardedIconComponent = memo(
|
|||
strokeWidth,
|
||||
id = "",
|
||||
}: IconComponentProps,
|
||||
ref,
|
||||
ref
|
||||
) => {
|
||||
const [showFallback, setShowFallback] = useState(false);
|
||||
|
||||
|
|
@ -65,8 +65,8 @@ export const ForwardedIconComponent = memo(
|
|||
/>
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default ForwardedIconComponent;
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
title: UPLOAD_ERROR_ALERT,
|
||||
list: [error],
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
@ -214,7 +214,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
|
||||
)}
|
||||
/>
|
||||
{printByBuildStatus()}
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ export default function HorizontalScrollFadeComponent({
|
|||
|
||||
fadeContainerRef.current.classList.toggle(
|
||||
"fade-left",
|
||||
isScrollable && !atStart,
|
||||
isScrollable && !atStart
|
||||
);
|
||||
fadeContainerRef.current.classList.toggle(
|
||||
"fade-right",
|
||||
isScrollable && !atEnd,
|
||||
isScrollable && !atEnd
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -104,8 +104,8 @@ export default function InputFileComponent({
|
|||
editNode
|
||||
? "input-edit-node input-dialog text-muted-foreground"
|
||||
: disabled
|
||||
? "input-disable input-dialog primary-input"
|
||||
: "input-dialog primary-input text-muted-foreground"
|
||||
? "input-disable input-dialog primary-input"
|
||||
: "input-dialog primary-input text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "No file"}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@ export default function InputGlobalComponent({
|
|||
editNode = false,
|
||||
}: InputGlobalComponentType): JSX.Element {
|
||||
const globalVariablesEntries = useGlobalVariablesStore(
|
||||
(state) => state.globalVariablesEntries,
|
||||
(state) => state.globalVariablesEntries
|
||||
);
|
||||
|
||||
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
|
||||
const unavaliableFields = useGlobalVariablesStore(
|
||||
(state) => state.unavaliableFields,
|
||||
(state) => state.unavaliableFields
|
||||
);
|
||||
const removeGlobalVariable = useGlobalVariablesStore(
|
||||
(state) => state.removeGlobalVariable,
|
||||
(state) => state.removeGlobalVariable
|
||||
);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ export default function InputGlobalComponent({
|
|||
<ForwardedIconComponent
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100",
|
||||
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { ColDef, ColGroupDef } from "ag-grid-community";
|
|||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
|
||||
import { FlowPoolObjectType } from "../../types/chat";
|
||||
import TableComponent from "../tableComponent";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
import TableComponent from "../tableComponent";
|
||||
|
||||
function RecordsOutputComponent({
|
||||
flowPool,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const SideBarButtonsComponent = ({ items }: SideBarButtonsComponentProps) => {
|
|||
data-testid={`sidebar-nav-${item.title}`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"!w-[200px] cursor-pointer justify-start gap-2 border border-transparent hover:border-border hover:bg-transparent",
|
||||
"!w-[200px] cursor-pointer justify-start gap-2 border border-transparent hover:border-border hover:bg-transparent"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import DateReader from "../dateReaderComponent";
|
|||
import NumberReader from "../numberReader";
|
||||
import ObjectRender from "../objectRender";
|
||||
import StringReader from "../stringReaderComponent";
|
||||
import { Label } from "../ui/label";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
export default function TableAutoCellRender({
|
||||
|
|
@ -35,7 +34,7 @@ export default function TableAutoCellRender({
|
|||
variant="outline"
|
||||
size="sq"
|
||||
className={cn(
|
||||
"min-w-min bg-success-background text-success-foreground hover:bg-success-background",
|
||||
"min-w-min bg-success-background text-success-foreground hover:bg-success-background"
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function TagsSelector({
|
|||
className={cn(
|
||||
selectedTags.some((category) => category === tag.name)
|
||||
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
|
||||
: "",
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{tag.name}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
|
|
@ -15,7 +15,7 @@ const alertVariants = cva(
|
|||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
|
|
@ -55,4 +55,4 @@ const AlertDescription = React.forwardRef<
|
|||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
export { Alert, AlertDescription, AlertTitle };
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -37,7 +37,7 @@ const CheckBoxDiv = ({
|
|||
className={cn(
|
||||
className,
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-primary text-primary-foreground" : "",
|
||||
checked ? "bg-primary text-primary-foreground" : ""
|
||||
)}
|
||||
>
|
||||
{checked && (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const AccordionTrigger = React.forwardRef<
|
|||
<div
|
||||
className={cn(
|
||||
" flex flex-1 cursor-pointer items-center justify-between border-[1px] py-2 text-sm font-medium data-[state=closed]:rounded-md data-[state=open]:rounded-t-md data-[state=open]:border-b-0 data-[state=open]:bg-muted [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -43,7 +43,7 @@ const AccordionContent = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden border-[1px] text-sm data-[state=open]:rounded-b-md data-[state=open]:border-t-0 data-[state=open]:bg-muted",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,18 +16,18 @@ const Form = FormProvider;
|
|||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
{} as FormFieldContextValue
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
|
|
@ -66,7 +66,7 @@ type FormItemContextValue = {
|
|||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
{} as FormItemContextValue
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
|
|
|
|||
|
|
@ -606,84 +606,6 @@ export const CONTROL_NEW_USER = {
|
|||
|
||||
export const tabsCode = [];
|
||||
|
||||
export function tabsArray(codes: string[], method: number) {
|
||||
if (!method) return;
|
||||
if (method === 0) {
|
||||
return [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
code: codes[0],
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
image:
|
||||
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
|
||||
language: "py",
|
||||
code: codes[1],
|
||||
},
|
||||
{
|
||||
name: "Python Code",
|
||||
mode: "python",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[2],
|
||||
},
|
||||
{
|
||||
name: "Chat Widget HTML",
|
||||
description:
|
||||
"Insert this code anywhere in your <body> tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
|
||||
mode: "html",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
code: codes[0],
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
image:
|
||||
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
|
||||
language: "py",
|
||||
code: codes[1],
|
||||
},
|
||||
{
|
||||
name: "Python Code",
|
||||
mode: "python",
|
||||
language: "py",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
code: codes[2],
|
||||
},
|
||||
{
|
||||
name: "Chat Widget HTML",
|
||||
description:
|
||||
"Insert this code anywhere in your <body> tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
|
||||
mode: "html",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
},
|
||||
{
|
||||
name: "Tweaks",
|
||||
mode: "python",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[4],
|
||||
},
|
||||
];
|
||||
}
|
||||
export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
|
||||
export const FETCH_ERROR_DESCRIPION =
|
||||
"Check if everything is working properly and try again.";
|
||||
|
|
|
|||
|
|
@ -355,8 +355,7 @@ export default function ParameterComponent({
|
|||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name]?.refresh_button_text ??
|
||||
"Refresh"
|
||||
data.node?.template[name].refresh_button_text
|
||||
}
|
||||
className="extra-side-bar-buttons mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
|
|
@ -404,8 +403,7 @@ export default function ParameterComponent({
|
|||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name]?.refresh_button_text ??
|
||||
"Refresh"
|
||||
data.node?.template[name].refresh_button_text
|
||||
}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const TooltipRenderComponent = ({ item, index, left }) => {
|
|||
<span
|
||||
key={index}
|
||||
className={classNames(
|
||||
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center",
|
||||
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const useFetchDataOnMount = (
|
|||
handleUpdateValues,
|
||||
setNode,
|
||||
renderTooltips,
|
||||
setIsLoading,
|
||||
setIsLoading
|
||||
) => {
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const useHandleNodeClass = (
|
|||
takeSnapshot,
|
||||
setNode,
|
||||
updateNodeInternals,
|
||||
renderTooltips,
|
||||
renderTooltips
|
||||
) => {
|
||||
const handleNodeClass = (newNodeClass, code) => {
|
||||
if (!data.node) return;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* @param {boolean} isAuth - If the API is authenticated
|
||||
* @returns {string} - The curl code
|
||||
*/
|
||||
export default function getCurlCode(
|
||||
export default function getCurlRunCode(
|
||||
flowId: string,
|
||||
isAuth: boolean,
|
||||
tweaksBuildedObject,
|
||||
|
|
@ -25,3 +25,24 @@ export default function getCurlCode(
|
|||
"tweaks": ${JSON.stringify(tweaksObject, null, 2)}}'
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cURL command for making a POST request to a webhook endpoint.
|
||||
*
|
||||
* @param {Object} options - The options for generating the cURL command.
|
||||
* @param {string} options.flowId - The ID of the flow.
|
||||
* @param {boolean} options.isAuth - Indicates whether authentication is required.
|
||||
* @param {string} options.endpointName - The name of the webhook endpoint.
|
||||
* @returns {string} The cURL command.
|
||||
*/
|
||||
export function getCurlWebhookCode(flowId, isAuth, endpointName?: string) {
|
||||
return `curl -X POST \\
|
||||
"${window.location.protocol}//${window.location.host}/api/v1/webhook/${
|
||||
endpointName || flowId
|
||||
}" \\
|
||||
-H 'Content-Type: application/json'\\${
|
||||
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
||||
}
|
||||
-d '{"any": "data"}'
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,11 @@
|
|||
export default function tabsArray(codes: string[], method: number) {
|
||||
if (!method) return;
|
||||
if (method === 0) {
|
||||
return [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
code: codes[0],
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
image:
|
||||
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
|
||||
language: "py",
|
||||
code: codes[1],
|
||||
},
|
||||
{
|
||||
name: "Python Code",
|
||||
mode: "python",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[2],
|
||||
},
|
||||
{
|
||||
name: "Chat Widget HTML",
|
||||
description:
|
||||
"Insert this code anywhere in your <body> tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
|
||||
mode: "html",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
export function createTabsArray(
|
||||
codes,
|
||||
includeWebhookCurl = false,
|
||||
includeTweaks = false
|
||||
) {
|
||||
const tabs = [
|
||||
{
|
||||
name: "cURL",
|
||||
name: "Run cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
|
|
@ -49,14 +17,14 @@ export default function tabsArray(codes: string[], method: number) {
|
|||
image:
|
||||
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
|
||||
language: "py",
|
||||
code: codes[1],
|
||||
code: codes[2],
|
||||
},
|
||||
{
|
||||
name: "Python Code",
|
||||
mode: "python",
|
||||
language: "py",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
code: codes[2],
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
},
|
||||
{
|
||||
name: "Chat Widget HTML",
|
||||
|
|
@ -64,15 +32,30 @@ export default function tabsArray(codes: string[], method: number) {
|
|||
"Insert this code anywhere in your <body> tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
|
||||
mode: "html",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
language: "html",
|
||||
code: codes[4],
|
||||
},
|
||||
{
|
||||
];
|
||||
|
||||
if (includeWebhookCurl) {
|
||||
tabs.splice(1, 0, {
|
||||
name: "Webhook cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
code: codes[1],
|
||||
});
|
||||
}
|
||||
|
||||
if (includeTweaks) {
|
||||
tabs.push({
|
||||
name: "Tweaks",
|
||||
mode: "python",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[4],
|
||||
},
|
||||
];
|
||||
code: codes[5],
|
||||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import { buildContent } from "../utils/build-content";
|
|||
import { buildTweaks } from "../utils/build-tweaks";
|
||||
import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object";
|
||||
import { getChangesType } from "../utils/get-changes-types";
|
||||
import getCurlCode from "../utils/get-curl-code";
|
||||
import { getCurlRunCode, getCurlWebhookCode } from "../utils/get-curl-code";
|
||||
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
|
||||
import getPythonApiCode from "../utils/get-python-api-code";
|
||||
import getPythonCode from "../utils/get-python-code";
|
||||
import { getValue } from "../utils/get-value";
|
||||
import getWidgetCode from "../utils/get-widget-code";
|
||||
import tabsArray from "../utils/tabs-array";
|
||||
import { createTabsArray } from "../utils/tabs-array";
|
||||
|
||||
const ApiModal = forwardRef(
|
||||
(
|
||||
|
|
@ -47,23 +47,33 @@ const ApiModal = forwardRef(
|
|||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak);
|
||||
const curl_code = getCurlCode(
|
||||
const curl_run_code = getCurlRunCode(
|
||||
flow?.id,
|
||||
autoLogin,
|
||||
tweak,
|
||||
flow?.endpoint_name
|
||||
);
|
||||
const curl_webhook_code = getCurlWebhookCode(
|
||||
flow?.id,
|
||||
autoLogin,
|
||||
flow?.endpoint_name
|
||||
);
|
||||
const pythonCode = getPythonCode(flow?.name, tweak);
|
||||
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
||||
console.log("flow", flow);
|
||||
const includeWebhook = flow.webhook;
|
||||
const tweaksCode = buildTweaks(flow);
|
||||
const codesArray = [
|
||||
curl_code,
|
||||
curl_run_code,
|
||||
curl_webhook_code,
|
||||
pythonApiCode,
|
||||
pythonCode,
|
||||
widgetCode,
|
||||
pythonCode,
|
||||
];
|
||||
const [tabs, setTabs] = useState(tabsArray(codesArray, 0));
|
||||
const [tabs, setTabs] = useState(
|
||||
createTabsArray(codesArray, includeWebhook)
|
||||
);
|
||||
|
||||
const canShowTweaks =
|
||||
flow &&
|
||||
|
|
@ -93,9 +103,9 @@ const ApiModal = forwardRef(
|
|||
|
||||
if (Object.keys(tweaksCode).length > 0) {
|
||||
setActiveTab("0");
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
setTabs(createTabsArray(codesArray, includeWebhook, true));
|
||||
} else {
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
setTabs(createTabsArray(codesArray, includeWebhook, true));
|
||||
}
|
||||
}, [flow["data"]!["nodes"], open]);
|
||||
|
||||
|
|
@ -166,7 +176,7 @@ const ApiModal = forwardRef(
|
|||
|
||||
const addCodes = (cloneTweak) => {
|
||||
const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, cloneTweak);
|
||||
const curl_code = getCurlCode(
|
||||
const curl_code = getCurlRunCode(
|
||||
flow?.id,
|
||||
autoLogin,
|
||||
cloneTweak,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type FlowType = {
|
|||
icon?: string;
|
||||
icon_bg_color?: string;
|
||||
folder_id?: string;
|
||||
webhook?: boolean;
|
||||
};
|
||||
|
||||
export type NodeType = {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { ColDef, ColGroupDef } from "ag-grid-community";
|
|||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import TableAutoCellRender from "../components/tableAutoCellRender";
|
||||
import { priorityFields } from "../constants/constants";
|
||||
import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "../flow_constants";
|
||||
import { APIDataType, TemplateVariableType } from "../types/api";
|
||||
import {
|
||||
groupedObjType,
|
||||
|
|
@ -57,7 +55,7 @@ export function normalCaseToSnakeCase(str: string): string {
|
|||
|
||||
export function toTitleCase(
|
||||
str: string | undefined,
|
||||
isNodeField?: boolean,
|
||||
isNodeField?: boolean
|
||||
): string {
|
||||
if (!str) return "";
|
||||
let result = str
|
||||
|
|
@ -66,7 +64,7 @@ export function toTitleCase(
|
|||
if (isNodeField) return word;
|
||||
if (index === 0) {
|
||||
return checkUpperWords(
|
||||
word[0].toUpperCase() + word.slice(1).toLowerCase(),
|
||||
word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
);
|
||||
}
|
||||
return checkUpperWords(word.toLowerCase());
|
||||
|
|
@ -79,7 +77,7 @@ export function toTitleCase(
|
|||
if (isNodeField) return word;
|
||||
if (index === 0) {
|
||||
return checkUpperWords(
|
||||
word[0].toUpperCase() + word.slice(1).toLowerCase(),
|
||||
word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
);
|
||||
}
|
||||
return checkUpperWords(word.toLowerCase());
|
||||
|
|
@ -183,7 +181,7 @@ export function checkLocalStorageKey(key: string): boolean {
|
|||
|
||||
export function IncrementObjectKey(
|
||||
object: object,
|
||||
key: string,
|
||||
key: string
|
||||
): { newKey: string; increment: number } {
|
||||
let count = 1;
|
||||
const type = removeCountFromString(key);
|
||||
|
|
@ -218,7 +216,7 @@ export function groupByFamily(
|
|||
data: APIDataType,
|
||||
baseClasses: string,
|
||||
left: boolean,
|
||||
flow?: NodeType[],
|
||||
flow?: NodeType[]
|
||||
): groupedObjType[] {
|
||||
const baseClassesSet = new Set(baseClasses.split("\n"));
|
||||
let arrOfPossibleInputs: Array<{
|
||||
|
|
@ -244,7 +242,7 @@ export function groupByFamily(
|
|||
baseClassesSet.has(template.type)) ||
|
||||
(template.input_types &&
|
||||
template.input_types.some((inputType) =>
|
||||
baseClassesSet.has(inputType),
|
||||
baseClassesSet.has(inputType)
|
||||
)))
|
||||
);
|
||||
};
|
||||
|
|
@ -264,7 +262,7 @@ export function groupByFamily(
|
|||
hasBaseClassInBaseClasses:
|
||||
foundNode?.hasBaseClassInBaseClasses ||
|
||||
nodeData.node!.base_classes.some((baseClass) =>
|
||||
baseClassesSet.has(baseClass),
|
||||
baseClassesSet.has(baseClass)
|
||||
), //seta como anterior ou verifica se o node tem base class
|
||||
displayName: nodeData.node?.display_name,
|
||||
});
|
||||
|
|
@ -281,10 +279,10 @@ export function groupByFamily(
|
|||
if (!foundNode) {
|
||||
foundNode = {
|
||||
hasBaseClassInTemplate: Object.values(node!.template).some(
|
||||
checkBaseClass,
|
||||
checkBaseClass
|
||||
),
|
||||
hasBaseClassInBaseClasses: node!.base_classes.some((baseClass) =>
|
||||
baseClassesSet.has(baseClass),
|
||||
baseClassesSet.has(baseClass)
|
||||
),
|
||||
displayName: node?.display_name,
|
||||
};
|
||||
|
|
@ -353,7 +351,7 @@ export function isTimeStampString(str: string): boolean {
|
|||
|
||||
export function extractColumnsFromRows(
|
||||
rows: object[],
|
||||
mode: "intersection" | "union",
|
||||
mode: "intersection" | "union"
|
||||
): (ColDef<any> | ColGroupDef<any>)[] {
|
||||
const columnsKeys: { [key: string]: ColDef<any> | ColGroupDef<any> } = {};
|
||||
if (rows.length === 0) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from uuid import UUID, uuid4
|
|||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from langflow.custom.directory_reader.directory_reader import DirectoryReader
|
||||
from langflow.services.deps import get_settings_service
|
||||
|
||||
|
|
@ -685,7 +684,7 @@ def test_run_flow_with_caching_invalid_flow_id(client: TestClient, created_api_k
|
|||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
data = response.json()
|
||||
assert "detail" in data
|
||||
assert f"Flow {invalid_flow_id} not found" in data["detail"]
|
||||
assert f"Flow identifier {invalid_flow_id} not found" in data["detail"]
|
||||
|
||||
|
||||
def test_run_flow_with_caching_invalid_input_format(client: TestClient, starter_project, created_api_key):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue