Merge branch 'langgenius:main' into add-document-status-update
commit
4d3a9bfa16
@ -0,0 +1,15 @@
|
||||
"""
|
||||
Repository interfaces for data access.
|
||||
|
||||
This package contains repository interfaces that define the contract
|
||||
for accessing and manipulating data, regardless of the underlying
|
||||
storage mechanism.
|
||||
"""
|
||||
|
||||
from core.repository.repository_factory import RepositoryFactory
|
||||
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
|
||||
|
||||
__all__ = [
|
||||
"RepositoryFactory",
|
||||
"WorkflowNodeExecutionRepository",
|
||||
]
|
||||
@ -0,0 +1,97 @@
|
||||
"""
|
||||
Repository factory for creating repository instances.
|
||||
|
||||
This module provides a simple factory interface for creating repository instances.
|
||||
It does not contain any implementation details or dependencies on specific repositories.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any, Literal, Optional, cast
|
||||
|
||||
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
|
||||
|
||||
# Type for factory functions - takes a dict of parameters and returns any repository type
|
||||
RepositoryFactoryFunc = Callable[[Mapping[str, Any]], Any]
|
||||
|
||||
# Type for workflow node execution factory function
|
||||
WorkflowNodeExecutionFactoryFunc = Callable[[Mapping[str, Any]], WorkflowNodeExecutionRepository]
|
||||
|
||||
# Repository type literals
|
||||
_RepositoryType = Literal["workflow_node_execution"]
|
||||
|
||||
|
||||
class RepositoryFactory:
|
||||
"""
|
||||
Factory class for creating repository instances.
|
||||
|
||||
This factory delegates the actual repository creation to implementation-specific
|
||||
factory functions that are registered with the factory at runtime.
|
||||
"""
|
||||
|
||||
# Dictionary to store factory functions
|
||||
_factory_functions: dict[str, RepositoryFactoryFunc] = {}
|
||||
|
||||
@classmethod
|
||||
def _register_factory(cls, repository_type: _RepositoryType, factory_func: RepositoryFactoryFunc) -> None:
|
||||
"""
|
||||
Register a factory function for a specific repository type.
|
||||
This is a private method and should not be called directly.
|
||||
|
||||
Args:
|
||||
repository_type: The type of repository (e.g., 'workflow_node_execution')
|
||||
factory_func: A function that takes parameters and returns a repository instance
|
||||
"""
|
||||
cls._factory_functions[repository_type] = factory_func
|
||||
|
||||
@classmethod
|
||||
def _create_repository(cls, repository_type: _RepositoryType, params: Optional[Mapping[str, Any]] = None) -> Any:
|
||||
"""
|
||||
Create a new repository instance with the provided parameters.
|
||||
This is a private method and should not be called directly.
|
||||
|
||||
Args:
|
||||
repository_type: The type of repository to create
|
||||
params: A dictionary of parameters to pass to the factory function
|
||||
|
||||
Returns:
|
||||
A new instance of the requested repository
|
||||
|
||||
Raises:
|
||||
ValueError: If no factory function is registered for the repository type
|
||||
"""
|
||||
if repository_type not in cls._factory_functions:
|
||||
raise ValueError(f"No factory function registered for repository type '{repository_type}'")
|
||||
|
||||
# Use empty dict if params is None
|
||||
params = params or {}
|
||||
|
||||
return cls._factory_functions[repository_type](params)
|
||||
|
||||
@classmethod
|
||||
def register_workflow_node_execution_factory(cls, factory_func: WorkflowNodeExecutionFactoryFunc) -> None:
|
||||
"""
|
||||
Register a factory function for the workflow node execution repository.
|
||||
|
||||
Args:
|
||||
factory_func: A function that takes parameters and returns a WorkflowNodeExecutionRepository instance
|
||||
"""
|
||||
cls._register_factory("workflow_node_execution", factory_func)
|
||||
|
||||
@classmethod
|
||||
def create_workflow_node_execution_repository(
|
||||
cls, params: Optional[Mapping[str, Any]] = None
|
||||
) -> WorkflowNodeExecutionRepository:
|
||||
"""
|
||||
Create a new WorkflowNodeExecutionRepository instance with the provided parameters.
|
||||
|
||||
Args:
|
||||
params: A dictionary of parameters to pass to the factory function
|
||||
|
||||
Returns:
|
||||
A new instance of the WorkflowNodeExecutionRepository
|
||||
|
||||
Raises:
|
||||
ValueError: If no factory function is registered for the workflow_node_execution repository type
|
||||
"""
|
||||
# We can safely cast here because we've registered a WorkflowNodeExecutionFactoryFunc
|
||||
return cast(WorkflowNodeExecutionRepository, cls._create_repository("workflow_node_execution", params))
|
||||
@ -0,0 +1,88 @@
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Optional, Protocol
|
||||
|
||||
from models.workflow import WorkflowNodeExecution
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderConfig:
|
||||
"""Configuration for ordering WorkflowNodeExecution instances."""
|
||||
|
||||
order_by: list[str]
|
||||
order_direction: Optional[Literal["asc", "desc"]] = None
|
||||
|
||||
|
||||
class WorkflowNodeExecutionRepository(Protocol):
|
||||
"""
|
||||
Repository interface for WorkflowNodeExecution.
|
||||
|
||||
This interface defines the contract for accessing and manipulating
|
||||
WorkflowNodeExecution data, regardless of the underlying storage mechanism.
|
||||
|
||||
Note: Domain-specific concepts like multi-tenancy (tenant_id), application context (app_id),
|
||||
and trigger sources (triggered_from) should be handled at the implementation level, not in
|
||||
the core interface. This keeps the core domain model clean and independent of specific
|
||||
application domains or deployment scenarios.
|
||||
"""
|
||||
|
||||
def save(self, execution: WorkflowNodeExecution) -> None:
|
||||
"""
|
||||
Save a WorkflowNodeExecution instance.
|
||||
|
||||
Args:
|
||||
execution: The WorkflowNodeExecution instance to save
|
||||
"""
|
||||
...
|
||||
|
||||
def get_by_node_execution_id(self, node_execution_id: str) -> Optional[WorkflowNodeExecution]:
|
||||
"""
|
||||
Retrieve a WorkflowNodeExecution by its node_execution_id.
|
||||
|
||||
Args:
|
||||
node_execution_id: The node execution ID
|
||||
|
||||
Returns:
|
||||
The WorkflowNodeExecution instance if found, None otherwise
|
||||
"""
|
||||
...
|
||||
|
||||
def get_by_workflow_run(
|
||||
self,
|
||||
workflow_run_id: str,
|
||||
order_config: Optional[OrderConfig] = None,
|
||||
) -> Sequence[WorkflowNodeExecution]:
|
||||
"""
|
||||
Retrieve all WorkflowNodeExecution instances for a specific workflow run.
|
||||
|
||||
Args:
|
||||
workflow_run_id: The workflow run ID
|
||||
order_config: Optional configuration for ordering results
|
||||
order_config.order_by: List of fields to order by (e.g., ["index", "created_at"])
|
||||
order_config.order_direction: Direction to order ("asc" or "desc")
|
||||
|
||||
Returns:
|
||||
A list of WorkflowNodeExecution instances
|
||||
"""
|
||||
...
|
||||
|
||||
def get_running_executions(self, workflow_run_id: str) -> Sequence[WorkflowNodeExecution]:
|
||||
"""
|
||||
Retrieve all running WorkflowNodeExecution instances for a specific workflow run.
|
||||
|
||||
Args:
|
||||
workflow_run_id: The workflow run ID
|
||||
|
||||
Returns:
|
||||
A list of running WorkflowNodeExecution instances
|
||||
"""
|
||||
...
|
||||
|
||||
def update(self, execution: WorkflowNodeExecution) -> None:
|
||||
"""
|
||||
Update an existing WorkflowNodeExecution instance.
|
||||
|
||||
Args:
|
||||
execution: The WorkflowNodeExecution instance to update
|
||||
"""
|
||||
...
|
||||
@ -0,0 +1,18 @@
|
||||
"""
|
||||
Extension for initializing repositories.
|
||||
|
||||
This extension registers repository implementations with the RepositoryFactory.
|
||||
"""
|
||||
|
||||
from dify_app import DifyApp
|
||||
from repositories.repository_registry import register_repositories
|
||||
|
||||
|
||||
def init_app(_app: DifyApp) -> None:
|
||||
"""
|
||||
Initialize repository implementations.
|
||||
|
||||
Args:
|
||||
_app: The Flask application instance (unused)
|
||||
"""
|
||||
register_repositories()
|
||||
@ -0,0 +1,6 @@
|
||||
"""
|
||||
Repository implementations for data access.
|
||||
|
||||
This package contains concrete implementations of the repository interfaces
|
||||
defined in the core.repository package.
|
||||
"""
|
||||
@ -0,0 +1,87 @@
|
||||
"""
|
||||
Registry for repository implementations.
|
||||
|
||||
This module is responsible for registering factory functions with the repository factory.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from core.repository.repository_factory import RepositoryFactory
|
||||
from extensions.ext_database import db
|
||||
from repositories.workflow_node_execution import SQLAlchemyWorkflowNodeExecutionRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Storage type constants
|
||||
STORAGE_TYPE_RDBMS = "rdbms"
|
||||
STORAGE_TYPE_HYBRID = "hybrid"
|
||||
|
||||
|
||||
def register_repositories() -> None:
|
||||
"""
|
||||
Register repository factory functions with the RepositoryFactory.
|
||||
|
||||
This function reads configuration settings to determine which repository
|
||||
implementations to register.
|
||||
"""
|
||||
# Configure WorkflowNodeExecutionRepository factory based on configuration
|
||||
workflow_node_execution_storage = dify_config.WORKFLOW_NODE_EXECUTION_STORAGE
|
||||
|
||||
# Check storage type and register appropriate implementation
|
||||
if workflow_node_execution_storage == STORAGE_TYPE_RDBMS:
|
||||
# Register SQLAlchemy implementation for RDBMS storage
|
||||
logger.info("Registering WorkflowNodeExecution repository with RDBMS storage")
|
||||
RepositoryFactory.register_workflow_node_execution_factory(create_workflow_node_execution_repository)
|
||||
elif workflow_node_execution_storage == STORAGE_TYPE_HYBRID:
|
||||
# Hybrid storage is not yet implemented
|
||||
raise NotImplementedError("Hybrid storage for WorkflowNodeExecution repository is not yet implemented")
|
||||
else:
|
||||
# Unknown storage type
|
||||
raise ValueError(
|
||||
f"Unknown storage type '{workflow_node_execution_storage}' for WorkflowNodeExecution repository. "
|
||||
f"Supported types: {STORAGE_TYPE_RDBMS}"
|
||||
)
|
||||
|
||||
|
||||
def create_workflow_node_execution_repository(params: Mapping[str, Any]) -> SQLAlchemyWorkflowNodeExecutionRepository:
|
||||
"""
|
||||
Create a WorkflowNodeExecutionRepository instance using SQLAlchemy implementation.
|
||||
|
||||
This factory function creates a repository for the RDBMS storage type.
|
||||
|
||||
Args:
|
||||
params: Parameters for creating the repository, including:
|
||||
- tenant_id: Required. The tenant ID for multi-tenancy.
|
||||
- app_id: Optional. The application ID for filtering.
|
||||
- session_factory: Optional. A SQLAlchemy sessionmaker instance. If not provided,
|
||||
a new sessionmaker will be created using the global database engine.
|
||||
|
||||
Returns:
|
||||
A WorkflowNodeExecutionRepository instance
|
||||
|
||||
Raises:
|
||||
ValueError: If required parameters are missing
|
||||
"""
|
||||
# Extract required parameters
|
||||
tenant_id = params.get("tenant_id")
|
||||
if tenant_id is None:
|
||||
raise ValueError("tenant_id is required for WorkflowNodeExecution repository with RDBMS storage")
|
||||
|
||||
# Extract optional parameters
|
||||
app_id = params.get("app_id")
|
||||
|
||||
# Use the session_factory from params if provided, otherwise create one using the global db engine
|
||||
session_factory = params.get("session_factory")
|
||||
if session_factory is None:
|
||||
# Create a sessionmaker using the same engine as the global db session
|
||||
session_factory = sessionmaker(bind=db.engine)
|
||||
|
||||
# Create and return the repository
|
||||
return SQLAlchemyWorkflowNodeExecutionRepository(
|
||||
session_factory=session_factory, tenant_id=tenant_id, app_id=app_id
|
||||
)
|
||||
@ -0,0 +1,9 @@
|
||||
"""
|
||||
WorkflowNodeExecution repository implementations.
|
||||
"""
|
||||
|
||||
from repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
|
||||
|
||||
__all__ = [
|
||||
"SQLAlchemyWorkflowNodeExecutionRepository",
|
||||
]
|
||||
@ -0,0 +1,170 @@
|
||||
"""
|
||||
SQLAlchemy implementation of the WorkflowNodeExecutionRepository.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import UnaryExpression, asc, desc, select
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.repository.workflow_node_execution_repository import OrderConfig
|
||||
from models.workflow import WorkflowNodeExecution, WorkflowNodeExecutionStatus, WorkflowNodeExecutionTriggeredFrom
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SQLAlchemyWorkflowNodeExecutionRepository:
|
||||
"""
|
||||
SQLAlchemy implementation of the WorkflowNodeExecutionRepository interface.
|
||||
|
||||
This implementation supports multi-tenancy by filtering operations based on tenant_id.
|
||||
Each method creates its own session, handles the transaction, and commits changes
|
||||
to the database. This prevents long-running connections in the workflow core.
|
||||
"""
|
||||
|
||||
def __init__(self, session_factory: sessionmaker | Engine, tenant_id: str, app_id: Optional[str] = None):
|
||||
"""
|
||||
Initialize the repository with a SQLAlchemy sessionmaker or engine and tenant context.
|
||||
|
||||
Args:
|
||||
session_factory: SQLAlchemy sessionmaker or engine for creating sessions
|
||||
tenant_id: Tenant ID for multi-tenancy
|
||||
app_id: Optional app ID for filtering by application
|
||||
"""
|
||||
# If an engine is provided, create a sessionmaker from it
|
||||
if isinstance(session_factory, Engine):
|
||||
self._session_factory = sessionmaker(bind=session_factory, expire_on_commit=False)
|
||||
else:
|
||||
self._session_factory = session_factory
|
||||
|
||||
self._tenant_id = tenant_id
|
||||
self._app_id = app_id
|
||||
|
||||
def save(self, execution: WorkflowNodeExecution) -> None:
|
||||
"""
|
||||
Save a WorkflowNodeExecution instance and commit changes to the database.
|
||||
|
||||
Args:
|
||||
execution: The WorkflowNodeExecution instance to save
|
||||
"""
|
||||
with self._session_factory() as session:
|
||||
# Ensure tenant_id is set
|
||||
if not execution.tenant_id:
|
||||
execution.tenant_id = self._tenant_id
|
||||
|
||||
# Set app_id if provided and not already set
|
||||
if self._app_id and not execution.app_id:
|
||||
execution.app_id = self._app_id
|
||||
|
||||
session.add(execution)
|
||||
session.commit()
|
||||
|
||||
def get_by_node_execution_id(self, node_execution_id: str) -> Optional[WorkflowNodeExecution]:
|
||||
"""
|
||||
Retrieve a WorkflowNodeExecution by its node_execution_id.
|
||||
|
||||
Args:
|
||||
node_execution_id: The node execution ID
|
||||
|
||||
Returns:
|
||||
The WorkflowNodeExecution instance if found, None otherwise
|
||||
"""
|
||||
with self._session_factory() as session:
|
||||
stmt = select(WorkflowNodeExecution).where(
|
||||
WorkflowNodeExecution.node_execution_id == node_execution_id,
|
||||
WorkflowNodeExecution.tenant_id == self._tenant_id,
|
||||
)
|
||||
|
||||
if self._app_id:
|
||||
stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id)
|
||||
|
||||
return session.scalar(stmt)
|
||||
|
||||
def get_by_workflow_run(
|
||||
self,
|
||||
workflow_run_id: str,
|
||||
order_config: Optional[OrderConfig] = None,
|
||||
) -> Sequence[WorkflowNodeExecution]:
|
||||
"""
|
||||
Retrieve all WorkflowNodeExecution instances for a specific workflow run.
|
||||
|
||||
Args:
|
||||
workflow_run_id: The workflow run ID
|
||||
order_config: Optional configuration for ordering results
|
||||
order_config.order_by: List of fields to order by (e.g., ["index", "created_at"])
|
||||
order_config.order_direction: Direction to order ("asc" or "desc")
|
||||
|
||||
Returns:
|
||||
A list of WorkflowNodeExecution instances
|
||||
"""
|
||||
with self._session_factory() as session:
|
||||
stmt = select(WorkflowNodeExecution).where(
|
||||
WorkflowNodeExecution.workflow_run_id == workflow_run_id,
|
||||
WorkflowNodeExecution.tenant_id == self._tenant_id,
|
||||
WorkflowNodeExecution.triggered_from == WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
||||
)
|
||||
|
||||
if self._app_id:
|
||||
stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id)
|
||||
|
||||
# Apply ordering if provided
|
||||
if order_config and order_config.order_by:
|
||||
order_columns: list[UnaryExpression] = []
|
||||
for field in order_config.order_by:
|
||||
column = getattr(WorkflowNodeExecution, field, None)
|
||||
if not column:
|
||||
continue
|
||||
if order_config.order_direction == "desc":
|
||||
order_columns.append(desc(column))
|
||||
else:
|
||||
order_columns.append(asc(column))
|
||||
|
||||
if order_columns:
|
||||
stmt = stmt.order_by(*order_columns)
|
||||
|
||||
return session.scalars(stmt).all()
|
||||
|
||||
def get_running_executions(self, workflow_run_id: str) -> Sequence[WorkflowNodeExecution]:
|
||||
"""
|
||||
Retrieve all running WorkflowNodeExecution instances for a specific workflow run.
|
||||
|
||||
Args:
|
||||
workflow_run_id: The workflow run ID
|
||||
|
||||
Returns:
|
||||
A list of running WorkflowNodeExecution instances
|
||||
"""
|
||||
with self._session_factory() as session:
|
||||
stmt = select(WorkflowNodeExecution).where(
|
||||
WorkflowNodeExecution.workflow_run_id == workflow_run_id,
|
||||
WorkflowNodeExecution.tenant_id == self._tenant_id,
|
||||
WorkflowNodeExecution.status == WorkflowNodeExecutionStatus.RUNNING,
|
||||
WorkflowNodeExecution.triggered_from == WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
||||
)
|
||||
|
||||
if self._app_id:
|
||||
stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id)
|
||||
|
||||
return session.scalars(stmt).all()
|
||||
|
||||
def update(self, execution: WorkflowNodeExecution) -> None:
|
||||
"""
|
||||
Update an existing WorkflowNodeExecution instance and commit changes to the database.
|
||||
|
||||
Args:
|
||||
execution: The WorkflowNodeExecution instance to update
|
||||
"""
|
||||
with self._session_factory() as session:
|
||||
# Ensure tenant_id is set
|
||||
if not execution.tenant_id:
|
||||
execution.tenant_id = self._tenant_id
|
||||
|
||||
# Set app_id if provided and not already set
|
||||
if self._app_id and not execution.app_id:
|
||||
execution.app_id = self._app_id
|
||||
|
||||
session.merge(execution)
|
||||
session.commit()
|
||||
@ -0,0 +1,99 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from core.model_runtime.entities.message_entities import AssistantPromptMessage
|
||||
from core.model_runtime.model_providers.__base.large_language_model import _increase_tool_call
|
||||
|
||||
ToolCall = AssistantPromptMessage.ToolCall
|
||||
|
||||
# CASE 1: Single tool call
|
||||
INPUTS_CASE_1 = [
|
||||
ToolCall(id="1", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments="")),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg1": ')),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
]
|
||||
EXPECTED_CASE_1 = [
|
||||
ToolCall(
|
||||
id="1", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments='{"arg1": "value"}')
|
||||
),
|
||||
]
|
||||
|
||||
# CASE 2: Tool call sequences where IDs are anchored to the first chunk (vLLM/SiliconFlow ...)
|
||||
INPUTS_CASE_2 = [
|
||||
ToolCall(id="1", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments="")),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg1": ')),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
ToolCall(id="2", type="function", function=ToolCall.ToolCallFunction(name="func_bar", arguments="")),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg2": ')),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
]
|
||||
EXPECTED_CASE_2 = [
|
||||
ToolCall(
|
||||
id="1", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments='{"arg1": "value"}')
|
||||
),
|
||||
ToolCall(
|
||||
id="2", type="function", function=ToolCall.ToolCallFunction(name="func_bar", arguments='{"arg2": "value"}')
|
||||
),
|
||||
]
|
||||
|
||||
# CASE 3: Tool call sequences where IDs are anchored to every chunk (SGLang ...)
|
||||
INPUTS_CASE_3 = [
|
||||
ToolCall(id="1", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments="")),
|
||||
ToolCall(id="1", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg1": ')),
|
||||
ToolCall(id="1", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
ToolCall(id="2", type="function", function=ToolCall.ToolCallFunction(name="func_bar", arguments="")),
|
||||
ToolCall(id="2", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg2": ')),
|
||||
ToolCall(id="2", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
]
|
||||
EXPECTED_CASE_3 = [
|
||||
ToolCall(
|
||||
id="1", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments='{"arg1": "value"}')
|
||||
),
|
||||
ToolCall(
|
||||
id="2", type="function", function=ToolCall.ToolCallFunction(name="func_bar", arguments='{"arg2": "value"}')
|
||||
),
|
||||
]
|
||||
|
||||
# CASE 4: Tool call sequences with no IDs
|
||||
INPUTS_CASE_4 = [
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="func_foo", arguments="")),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg1": ')),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="func_bar", arguments="")),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='{"arg2": ')),
|
||||
ToolCall(id="", type="function", function=ToolCall.ToolCallFunction(name="", arguments='"value"}')),
|
||||
]
|
||||
EXPECTED_CASE_4 = [
|
||||
ToolCall(
|
||||
id="RANDOM_ID_1",
|
||||
type="function",
|
||||
function=ToolCall.ToolCallFunction(name="func_foo", arguments='{"arg1": "value"}'),
|
||||
),
|
||||
ToolCall(
|
||||
id="RANDOM_ID_2",
|
||||
type="function",
|
||||
function=ToolCall.ToolCallFunction(name="func_bar", arguments='{"arg2": "value"}'),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _run_case(inputs: list[ToolCall], expected: list[ToolCall]):
|
||||
actual = []
|
||||
_increase_tool_call(inputs, actual)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test__increase_tool_call():
|
||||
# case 1:
|
||||
_run_case(INPUTS_CASE_1, EXPECTED_CASE_1)
|
||||
|
||||
# case 2:
|
||||
_run_case(INPUTS_CASE_2, EXPECTED_CASE_2)
|
||||
|
||||
# case 3:
|
||||
_run_case(INPUTS_CASE_3, EXPECTED_CASE_3)
|
||||
|
||||
# case 4:
|
||||
mock_id_generator = MagicMock()
|
||||
mock_id_generator.side_effect = [_exp_case.id for _exp_case in EXPECTED_CASE_4]
|
||||
with patch("core.model_runtime.model_providers.__base.large_language_model._gen_tool_call_id", mock_id_generator):
|
||||
_run_case(INPUTS_CASE_4, EXPECTED_CASE_4)
|
||||
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Unit tests for repositories.
|
||||
"""
|
||||
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Unit tests for workflow_node_execution repositories.
|
||||
"""
|
||||
@ -0,0 +1,154 @@
|
||||
"""
|
||||
Unit tests for the SQLAlchemy implementation of WorkflowNodeExecutionRepository.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from core.repository.workflow_node_execution_repository import OrderConfig
|
||||
from models.workflow import WorkflowNodeExecution
|
||||
from repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
"""Create a mock SQLAlchemy session."""
|
||||
session = MagicMock(spec=Session)
|
||||
# Configure the session to be used as a context manager
|
||||
session.__enter__ = MagicMock(return_value=session)
|
||||
session.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
# Configure the session factory to return the session
|
||||
session_factory = MagicMock(spec=sessionmaker)
|
||||
session_factory.return_value = session
|
||||
return session, session_factory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repository(session):
|
||||
"""Create a repository instance with test data."""
|
||||
_, session_factory = session
|
||||
tenant_id = "test-tenant"
|
||||
app_id = "test-app"
|
||||
return SQLAlchemyWorkflowNodeExecutionRepository(
|
||||
session_factory=session_factory, tenant_id=tenant_id, app_id=app_id
|
||||
)
|
||||
|
||||
|
||||
def test_save(repository, session):
|
||||
"""Test save method."""
|
||||
session_obj, _ = session
|
||||
# Create a mock execution
|
||||
execution = MagicMock(spec=WorkflowNodeExecution)
|
||||
execution.tenant_id = None
|
||||
execution.app_id = None
|
||||
|
||||
# Call save method
|
||||
repository.save(execution)
|
||||
|
||||
# Assert tenant_id and app_id are set
|
||||
assert execution.tenant_id == repository._tenant_id
|
||||
assert execution.app_id == repository._app_id
|
||||
|
||||
# Assert session.add was called
|
||||
session_obj.add.assert_called_once_with(execution)
|
||||
|
||||
|
||||
def test_save_with_existing_tenant_id(repository, session):
|
||||
"""Test save method with existing tenant_id."""
|
||||
session_obj, _ = session
|
||||
# Create a mock execution with existing tenant_id
|
||||
execution = MagicMock(spec=WorkflowNodeExecution)
|
||||
execution.tenant_id = "existing-tenant"
|
||||
execution.app_id = None
|
||||
|
||||
# Call save method
|
||||
repository.save(execution)
|
||||
|
||||
# Assert tenant_id is not changed and app_id is set
|
||||
assert execution.tenant_id == "existing-tenant"
|
||||
assert execution.app_id == repository._app_id
|
||||
|
||||
# Assert session.add was called
|
||||
session_obj.add.assert_called_once_with(execution)
|
||||
|
||||
|
||||
def test_get_by_node_execution_id(repository, session, mocker: MockerFixture):
|
||||
"""Test get_by_node_execution_id method."""
|
||||
session_obj, _ = session
|
||||
# Set up mock
|
||||
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select")
|
||||
mock_stmt = mocker.MagicMock()
|
||||
mock_select.return_value = mock_stmt
|
||||
mock_stmt.where.return_value = mock_stmt
|
||||
session_obj.scalar.return_value = mocker.MagicMock(spec=WorkflowNodeExecution)
|
||||
|
||||
# Call method
|
||||
result = repository.get_by_node_execution_id("test-node-execution-id")
|
||||
|
||||
# Assert select was called with correct parameters
|
||||
mock_select.assert_called_once()
|
||||
session_obj.scalar.assert_called_once_with(mock_stmt)
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_get_by_workflow_run(repository, session, mocker: MockerFixture):
|
||||
"""Test get_by_workflow_run method."""
|
||||
session_obj, _ = session
|
||||
# Set up mock
|
||||
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select")
|
||||
mock_stmt = mocker.MagicMock()
|
||||
mock_select.return_value = mock_stmt
|
||||
mock_stmt.where.return_value = mock_stmt
|
||||
mock_stmt.order_by.return_value = mock_stmt
|
||||
session_obj.scalars.return_value.all.return_value = [mocker.MagicMock(spec=WorkflowNodeExecution)]
|
||||
|
||||
# Call method
|
||||
order_config = OrderConfig(order_by=["index"], order_direction="desc")
|
||||
result = repository.get_by_workflow_run(workflow_run_id="test-workflow-run-id", order_config=order_config)
|
||||
|
||||
# Assert select was called with correct parameters
|
||||
mock_select.assert_called_once()
|
||||
session_obj.scalars.assert_called_once_with(mock_stmt)
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
def test_get_running_executions(repository, session, mocker: MockerFixture):
|
||||
"""Test get_running_executions method."""
|
||||
session_obj, _ = session
|
||||
# Set up mock
|
||||
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select")
|
||||
mock_stmt = mocker.MagicMock()
|
||||
mock_select.return_value = mock_stmt
|
||||
mock_stmt.where.return_value = mock_stmt
|
||||
session_obj.scalars.return_value.all.return_value = [mocker.MagicMock(spec=WorkflowNodeExecution)]
|
||||
|
||||
# Call method
|
||||
result = repository.get_running_executions("test-workflow-run-id")
|
||||
|
||||
# Assert select was called with correct parameters
|
||||
mock_select.assert_called_once()
|
||||
session_obj.scalars.assert_called_once_with(mock_stmt)
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
def test_update(repository, session):
|
||||
"""Test update method."""
|
||||
session_obj, _ = session
|
||||
# Create a mock execution
|
||||
execution = MagicMock(spec=WorkflowNodeExecution)
|
||||
execution.tenant_id = None
|
||||
execution.app_id = None
|
||||
|
||||
# Call update method
|
||||
repository.update(execution)
|
||||
|
||||
# Assert tenant_id and app_id are set
|
||||
assert execution.tenant_id == repository._tenant_id
|
||||
assert execution.app_id == repository._app_id
|
||||
|
||||
# Assert session.merge was called
|
||||
session_obj.merge.assert_called_once_with(execution)
|
||||
@ -0,0 +1,69 @@
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import Features from '@/app/components/workflow/features'
|
||||
import PluginDependency from '@/app/components/workflow/plugin-dependency'
|
||||
import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal'
|
||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||
import {
|
||||
useDSL,
|
||||
usePanelInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import WorkflowHeader from './workflow-header'
|
||||
import WorkflowPanel from './workflow-panel'
|
||||
|
||||
const WorkflowChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const showFeaturesPanel = useStore(s => s.showFeaturesPanel)
|
||||
const showImportDSLModal = useStore(s => s.showImportDSLModal)
|
||||
const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal)
|
||||
const {
|
||||
handlePaneContextmenuCancel,
|
||||
} = usePanelInteractions()
|
||||
const {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
} = useDSL()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === DSL_EXPORT_CHECK)
|
||||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginDependency />
|
||||
{
|
||||
showFeaturesPanel && <Features />
|
||||
}
|
||||
{
|
||||
showImportDSLModal && (
|
||||
<UpdateDSLModal
|
||||
onCancel={() => setShowImportDSLModal(false)}
|
||||
onBackup={exportCheck}
|
||||
onImport={handlePaneContextmenuCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={handleExportDSL}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<WorkflowHeader />
|
||||
<WorkflowPanel />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(WorkflowChildren)
|
||||
@ -0,0 +1,11 @@
|
||||
import { memo } from 'react'
|
||||
import ChatVariableButton from '@/app/components/workflow/header/chat-variable-button'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const ChatVariableTrigger = () => {
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
return <ChatVariableButton disabled={nodesReadOnly} />
|
||||
}
|
||||
export default memo(ChatVariableTrigger)
|
||||
@ -0,0 +1,152 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { RiApps2AddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import {
|
||||
useChecklistBeforePublish,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppPublisher from '@/app/components/app/app-publisher'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import {
|
||||
BlockEnum,
|
||||
InputVarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useSelector as useAppSelector } from '@/context/app-context'
|
||||
|
||||
const FeaturesTrigger = () => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appID = appDetail?.id
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const systemFeatures = useAppSelector(state => state.systemFeatures)
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const toolPublished = useStore(s => s.toolPublished)
|
||||
const nodes = useNodes<StartNodeType>()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables
|
||||
const fileSettings = useFeatures(s => s.features.file)
|
||||
const variables = useMemo(() => {
|
||||
const data = startVariables || []
|
||||
if (fileSettings?.image?.enabled) {
|
||||
return [
|
||||
...data,
|
||||
{
|
||||
type: InputVarType.files,
|
||||
variable: '__image',
|
||||
required: false,
|
||||
label: 'files',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return data
|
||||
}, [fileSettings?.image?.enabled, startVariables])
|
||||
|
||||
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const handleShowFeatures = useCallback(() => {
|
||||
const {
|
||||
showFeaturesPanel,
|
||||
isRestoring,
|
||||
setShowFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
if (getNodesReadOnly() && !isRestoring)
|
||||
return
|
||||
setShowFeaturesPanel(!showFeaturesPanel)
|
||||
}, [workflowStore, getNodesReadOnly])
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)
|
||||
|
||||
const updateAppDetail = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appID! })
|
||||
if (systemFeatures.enable_web_sso_switch_component) {
|
||||
const ssoRes = await fetchAppSSO({ appId: appID! })
|
||||
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
||||
}
|
||||
else {
|
||||
setAppDetail({ ...res })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component])
|
||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
|
||||
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
|
||||
if (await handleCheckBeforePublish()) {
|
||||
const res = await publishWorkflow({
|
||||
title: params?.title || '',
|
||||
releaseNotes: params?.releaseNotes || '',
|
||||
})
|
||||
|
||||
if (res) {
|
||||
notify({ type: 'success', message: t('common.api.actionSuccess') })
|
||||
updateAppDetail()
|
||||
workflowStore.getState().setPublishedAt(res.created_at)
|
||||
resetWorkflowVersionHistory()
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error('Checklist failed')
|
||||
}
|
||||
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
|
||||
|
||||
const onPublisherToggle = useCallback((state: boolean) => {
|
||||
if (state)
|
||||
handleSyncWorkflowDraft(true)
|
||||
}, [handleSyncWorkflowDraft])
|
||||
|
||||
const handleToolConfigureUpdate = useCallback(() => {
|
||||
workflowStore.setState({ toolPublished: true })
|
||||
}, [workflowStore])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
||||
<RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
<AppPublisher
|
||||
{...{
|
||||
publishedAt,
|
||||
draftUpdatedAt,
|
||||
disabled: nodesReadOnly,
|
||||
toolPublished,
|
||||
inputs: variables,
|
||||
onRefreshData: handleToolConfigureUpdate,
|
||||
onPublish,
|
||||
onToggle: onPublisherToggle,
|
||||
crossAxisOffset: 4,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FeaturesTrigger)
|
||||
@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||
import Header from '@/app/components/workflow/header'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import ChatVariableTrigger from './chat-variable-trigger'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
import { useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
|
||||
const WorkflowHeader = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)
|
||||
|
||||
const headerProps: HeaderProps = useMemo(() => {
|
||||
return {
|
||||
normal: {
|
||||
components: {
|
||||
left: <ChatVariableTrigger />,
|
||||
middle: <FeaturesTrigger />,
|
||||
},
|
||||
},
|
||||
restoring: {
|
||||
onRestoreSettled: resetWorkflowVersionHistory,
|
||||
},
|
||||
}
|
||||
}, [resetWorkflowVersionHistory])
|
||||
return (
|
||||
<Header {...headerProps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowHeader
|
||||
@ -0,0 +1,87 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import WorkflowChildren from './workflow-children'
|
||||
import {
|
||||
useNodesSyncDraft,
|
||||
useWorkflowRun,
|
||||
useWorkflowStartRun,
|
||||
} from '../hooks'
|
||||
|
||||
type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
|
||||
const WorkflowMain = ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
}: WorkflowMainProps) => {
|
||||
const featuresStore = useFeaturesStore()
|
||||
|
||||
const handleWorkflowDataUpdate = useCallback((payload: any) => {
|
||||
if (payload.features && featuresStore) {
|
||||
const { setFeatures } = featuresStore.getState()
|
||||
|
||||
setFeatures(payload.features)
|
||||
}
|
||||
}, [featuresStore])
|
||||
|
||||
const {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
} = useNodesSyncDraft()
|
||||
const {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
} = useWorkflowRun()
|
||||
const {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
} = useWorkflowStartRun()
|
||||
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
doSyncWorkflowDraft,
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
}
|
||||
}, [
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
doSyncWorkflowDraft,
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
])
|
||||
|
||||
return (
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
onWorkflowDataUpdate={handleWorkflowDataUpdate}
|
||||
hooksStore={hooksStore}
|
||||
>
|
||||
<WorkflowChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowMain
|
||||
@ -0,0 +1,109 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
} from '../hooks'
|
||||
import DebugAndPreview from '@/app/components/workflow/panel/debug-and-preview'
|
||||
import Record from '@/app/components/workflow/panel/record'
|
||||
import WorkflowPreview from '@/app/components/workflow/panel/workflow-preview'
|
||||
import ChatRecord from '@/app/components/workflow/panel/chat-record'
|
||||
import ChatVariablePanel from '@/app/components/workflow/panel/chat-variable-panel'
|
||||
import GlobalVariablePanel from '@/app/components/workflow/panel/global-variable-panel'
|
||||
import VersionHistoryPanel from '@/app/components/workflow/panel/version-history-panel'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import MessageLogModal from '@/app/components/base/message-log-modal'
|
||||
import type { PanelProps } from '@/app/components/workflow/panel'
|
||||
import Panel from '@/app/components/workflow/panel'
|
||||
|
||||
const WorkflowPanelOnLeft = () => {
|
||||
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
currentLogModalActiveTab: state.currentLogModalActiveTab,
|
||||
})))
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showMessageLogModal && (
|
||||
<MessageLogModal
|
||||
fixedWidth
|
||||
width={400}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const WorkflowPanelOnRight = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
const showChatVariablePanel = useStore(s => s.showChatVariablePanel)
|
||||
const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel)
|
||||
const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel)
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
historyWorkflowData && !isChatMode && (
|
||||
<Record />
|
||||
)
|
||||
}
|
||||
{
|
||||
historyWorkflowData && isChatMode && (
|
||||
<ChatRecord />
|
||||
)
|
||||
}
|
||||
{
|
||||
showDebugAndPreviewPanel && isChatMode && (
|
||||
<DebugAndPreview />
|
||||
)
|
||||
}
|
||||
{
|
||||
showDebugAndPreviewPanel && !isChatMode && (
|
||||
<WorkflowPreview />
|
||||
)
|
||||
}
|
||||
{
|
||||
showChatVariablePanel && (
|
||||
<ChatVariablePanel />
|
||||
)
|
||||
}
|
||||
{
|
||||
showGlobalVariablePanel && (
|
||||
<GlobalVariablePanel />
|
||||
)
|
||||
}
|
||||
{
|
||||
showWorkflowVersionHistoryPanel && (
|
||||
<VersionHistoryPanel/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const WorkflowPanel = () => {
|
||||
const panelProps: PanelProps = useMemo(() => {
|
||||
return {
|
||||
components: {
|
||||
left: <WorkflowPanelOnLeft />,
|
||||
right: <WorkflowPanelOnRight />,
|
||||
},
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Panel {...panelProps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowPanel
|
||||
@ -0,0 +1,6 @@
|
||||
export * from './use-workflow-init'
|
||||
export * from './use-workflow-template'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-is-chat-mode'
|
||||
@ -0,0 +1,7 @@
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
||||
return appDetail?.mode === 'advanced-chat'
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks/use-workflow'
|
||||
import { syncWorkflowDraft } from '@/service/workflow'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { API_PREFIX } from '@/config'
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { handleRefreshWorkflowDraft } = useWorkflowUpdate()
|
||||
const params = useParams()
|
||||
|
||||
const getPostParams = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const [x, y, zoom] = transform
|
||||
const {
|
||||
appId,
|
||||
conversationVariables,
|
||||
environmentVariables,
|
||||
syncWorkflowDraftHash,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (appId) {
|
||||
const nodes = getNodes()
|
||||
const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
|
||||
if (!hasStartNode)
|
||||
return
|
||||
|
||||
const features = featuresStore!.getState().features
|
||||
const producedNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
Object.keys(node.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete node.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
const producedEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
Object.keys(edge.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete edge.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
return {
|
||||
url: `/apps/${appId}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: producedNodes,
|
||||
edges: producedEdges,
|
||||
viewport: {
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
},
|
||||
features: {
|
||||
opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
|
||||
suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
|
||||
suggested_questions_after_answer: features.suggested,
|
||||
text_to_speech: features.text2speech,
|
||||
speech_to_text: features.speech2text,
|
||||
retriever_resource: features.citation,
|
||||
sensitive_word_avoidance: features.moderation,
|
||||
file_upload: features.file,
|
||||
},
|
||||
environment_variables: environmentVariables,
|
||||
conversation_variables: conversationVariables,
|
||||
hash: syncWorkflowDraftHash,
|
||||
},
|
||||
}
|
||||
}
|
||||
}, [store, featuresStore, workflowStore])
|
||||
|
||||
const syncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
const postParams = getPostParams()
|
||||
|
||||
if (postParams) {
|
||||
navigator.sendBeacon(
|
||||
`${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`,
|
||||
JSON.stringify(postParams.params),
|
||||
)
|
||||
}
|
||||
}, [getPostParams, params.appId, getNodesReadOnly])
|
||||
|
||||
const doSyncWorkflowDraft = useCallback(async (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
},
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
const postParams = getPostParams()
|
||||
|
||||
if (postParams) {
|
||||
const {
|
||||
setSyncWorkflowDraftHash,
|
||||
setDraftUpdatedAt,
|
||||
} = workflowStore.getState()
|
||||
try {
|
||||
const res = await syncWorkflowDraft(postParams)
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
callback?.onSuccess && callback.onSuccess()
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
|
||||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError && callback.onError()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled && callback.onSettled()
|
||||
}
|
||||
}
|
||||
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft])
|
||||
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { useWorkflowTemplate } from './use-workflow-template'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
fetchNodesDefaultConfigs,
|
||||
fetchPublishedWorkflow,
|
||||
fetchWorkflowDraft,
|
||||
syncWorkflowDraft,
|
||||
} from '@/service/workflow'
|
||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||
|
||||
export const useWorkflowInit = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
} = useWorkflowTemplate()
|
||||
const appDetail = useAppStore(state => state.appDetail)!
|
||||
const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
|
||||
const [data, setData] = useState<FetchWorkflowDraftResponse>()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
workflowStore.setState({ appId: appDetail.id })
|
||||
}, [appDetail.id, workflowStore])
|
||||
|
||||
const handleUpdateWorkflowConfig = useCallback((config: Record<string, any>) => {
|
||||
const { setWorkflowConfig } = workflowStore.getState()
|
||||
|
||||
setWorkflowConfig(config)
|
||||
}, [workflowStore])
|
||||
useWorkflowConfig(appDetail.id, handleUpdateWorkflowConfig)
|
||||
|
||||
const handleGetInitialWorkflowData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
|
||||
setData(res)
|
||||
workflowStore.setState({
|
||||
envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
return acc
|
||||
}, {} as Record<string, string>),
|
||||
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||
conversationVariables: res.conversation_variables || [],
|
||||
})
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setIsLoading(false)
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed && appDetail) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_exist') {
|
||||
workflowStore.setState({ notInitialWorkflow: true })
|
||||
syncWorkflowDraft({
|
||||
url: `/apps/${appDetail.id}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
},
|
||||
features: {
|
||||
retriever_resource: { enabled: true },
|
||||
},
|
||||
environment_variables: [],
|
||||
conversation_variables: [],
|
||||
},
|
||||
}).then((res) => {
|
||||
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
||||
handleGetInitialWorkflowData()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash])
|
||||
|
||||
useEffect(() => {
|
||||
handleGetInitialWorkflowData()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleFetchPreloadData = useCallback(async () => {
|
||||
try {
|
||||
const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
|
||||
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
|
||||
workflowStore.setState({
|
||||
nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => {
|
||||
if (!acc[block.type])
|
||||
acc[block.type] = { ...block.config }
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
})
|
||||
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, [workflowStore, appDetail])
|
||||
|
||||
useEffect(() => {
|
||||
handleFetchPreloadData()
|
||||
}, [handleFetchPreloadData])
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
workflowStore.getState().setDraftUpdatedAt(data.updated_at)
|
||||
workflowStore.getState().setToolPublished(data.tool_published)
|
||||
}
|
||||
}, [data, workflowStore])
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,357 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
|
||||
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import { ssePost } from '@/service/base'
|
||||
import { stopWorkflowRun } from '@/service/workflow'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
export const useWorkflowRun = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||
const pathname = usePathname()
|
||||
|
||||
const {
|
||||
handleWorkflowStarted,
|
||||
handleWorkflowFinished,
|
||||
handleWorkflowFailed,
|
||||
handleWorkflowNodeStarted,
|
||||
handleWorkflowNodeFinished,
|
||||
handleWorkflowNodeIterationStarted,
|
||||
handleWorkflowNodeIterationNext,
|
||||
handleWorkflowNodeIterationFinished,
|
||||
handleWorkflowNodeLoopStarted,
|
||||
handleWorkflowNodeLoopNext,
|
||||
handleWorkflowNodeLoopFinished,
|
||||
handleWorkflowNodeRetry,
|
||||
handleWorkflowAgentLog,
|
||||
handleWorkflowTextChunk,
|
||||
handleWorkflowTextReplace,
|
||||
} = useWorkflowRunEvent()
|
||||
|
||||
const handleBackupDraft = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const { getViewport } = reactflow
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
environmentVariables,
|
||||
} = workflowStore.getState()
|
||||
const { features } = featuresStore!.getState()
|
||||
|
||||
if (!backupDraft) {
|
||||
setBackupDraft({
|
||||
nodes: getNodes(),
|
||||
edges,
|
||||
viewport: getViewport(),
|
||||
features,
|
||||
environmentVariables,
|
||||
})
|
||||
doSyncWorkflowDraft()
|
||||
}
|
||||
}, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft])
|
||||
|
||||
const handleLoadBackupDraft = useCallback(() => {
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
setEnvironmentVariables,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (backupDraft) {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
features,
|
||||
environmentVariables,
|
||||
} = backupDraft
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
setEnvironmentVariables(environmentVariables)
|
||||
featuresStore!.setState({ features })
|
||||
setBackupDraft(undefined)
|
||||
}
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore, featuresStore])
|
||||
|
||||
const handleRun = useCallback(async (
|
||||
params: any,
|
||||
callback?: IOtherOptions,
|
||||
) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
node.data._runningStatus = undefined
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
await doSyncWorkflowDraft()
|
||||
|
||||
const {
|
||||
onWorkflowStarted,
|
||||
onWorkflowFinished,
|
||||
onNodeStarted,
|
||||
onNodeFinished,
|
||||
onIterationStart,
|
||||
onIterationNext,
|
||||
onIterationFinish,
|
||||
onLoopStart,
|
||||
onLoopNext,
|
||||
onLoopFinish,
|
||||
onNodeRetry,
|
||||
onAgentLog,
|
||||
onError,
|
||||
...restCallback
|
||||
} = callback || {}
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
|
||||
const {
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
} = workflowContainer!
|
||||
|
||||
let url = ''
|
||||
if (appDetail?.mode === 'advanced-chat')
|
||||
url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run`
|
||||
|
||||
if (appDetail?.mode === 'workflow')
|
||||
url = `/apps/${appDetail.id}/workflows/draft/run`
|
||||
|
||||
const {
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
setWorkflowRunningData({
|
||||
result: {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
},
|
||||
tracing: [],
|
||||
resultText: '',
|
||||
})
|
||||
|
||||
let ttsUrl = ''
|
||||
let ttsIsPublic = false
|
||||
if (params.token) {
|
||||
ttsUrl = '/text-to-audio'
|
||||
ttsIsPublic = true
|
||||
}
|
||||
else if (params.appId) {
|
||||
if (pathname.search('explore/installed') > -1)
|
||||
ttsUrl = `/installed-apps/${params.appId}/text-to-audio`
|
||||
else
|
||||
ttsUrl = `/apps/${params.appId}/text-to-audio`
|
||||
}
|
||||
const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', noop)
|
||||
|
||||
ssePost(
|
||||
url,
|
||||
{
|
||||
body: params,
|
||||
},
|
||||
{
|
||||
onWorkflowStarted: (params) => {
|
||||
handleWorkflowStarted(params)
|
||||
|
||||
if (onWorkflowStarted)
|
||||
onWorkflowStarted(params)
|
||||
},
|
||||
onWorkflowFinished: (params) => {
|
||||
handleWorkflowFinished(params)
|
||||
|
||||
if (onWorkflowFinished)
|
||||
onWorkflowFinished(params)
|
||||
},
|
||||
onError: (params) => {
|
||||
handleWorkflowFailed()
|
||||
|
||||
if (onError)
|
||||
onError(params)
|
||||
},
|
||||
onNodeStarted: (params) => {
|
||||
handleWorkflowNodeStarted(
|
||||
params,
|
||||
{
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
},
|
||||
)
|
||||
|
||||
if (onNodeStarted)
|
||||
onNodeStarted(params)
|
||||
},
|
||||
onNodeFinished: (params) => {
|
||||
handleWorkflowNodeFinished(params)
|
||||
|
||||
if (onNodeFinished)
|
||||
onNodeFinished(params)
|
||||
},
|
||||
onIterationStart: (params) => {
|
||||
handleWorkflowNodeIterationStarted(
|
||||
params,
|
||||
{
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
},
|
||||
)
|
||||
|
||||
if (onIterationStart)
|
||||
onIterationStart(params)
|
||||
},
|
||||
onIterationNext: (params) => {
|
||||
handleWorkflowNodeIterationNext(params)
|
||||
|
||||
if (onIterationNext)
|
||||
onIterationNext(params)
|
||||
},
|
||||
onIterationFinish: (params) => {
|
||||
handleWorkflowNodeIterationFinished(params)
|
||||
|
||||
if (onIterationFinish)
|
||||
onIterationFinish(params)
|
||||
},
|
||||
onLoopStart: (params) => {
|
||||
handleWorkflowNodeLoopStarted(
|
||||
params,
|
||||
{
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
},
|
||||
)
|
||||
|
||||
if (onLoopStart)
|
||||
onLoopStart(params)
|
||||
},
|
||||
onLoopNext: (params) => {
|
||||
handleWorkflowNodeLoopNext(params)
|
||||
|
||||
if (onLoopNext)
|
||||
onLoopNext(params)
|
||||
},
|
||||
onLoopFinish: (params) => {
|
||||
handleWorkflowNodeLoopFinished(params)
|
||||
|
||||
if (onLoopFinish)
|
||||
onLoopFinish(params)
|
||||
},
|
||||
onNodeRetry: (params) => {
|
||||
handleWorkflowNodeRetry(params)
|
||||
|
||||
if (onNodeRetry)
|
||||
onNodeRetry(params)
|
||||
},
|
||||
onAgentLog: (params) => {
|
||||
handleWorkflowAgentLog(params)
|
||||
|
||||
if (onAgentLog)
|
||||
onAgentLog(params)
|
||||
},
|
||||
onTextChunk: (params) => {
|
||||
handleWorkflowTextChunk(params)
|
||||
},
|
||||
onTextReplace: (params) => {
|
||||
handleWorkflowTextReplace(params)
|
||||
},
|
||||
onTTSChunk: (messageId: string, audio: string) => {
|
||||
if (!audio || audio === '')
|
||||
return
|
||||
player.playAudioWithAudio(audio, true)
|
||||
AudioPlayerManager.getInstance().resetMsgId(messageId)
|
||||
},
|
||||
onTTSEnd: (messageId: string, audio: string) => {
|
||||
player.playAudioWithAudio(audio, false)
|
||||
},
|
||||
...restCallback,
|
||||
},
|
||||
)
|
||||
}, [
|
||||
store,
|
||||
workflowStore,
|
||||
doSyncWorkflowDraft,
|
||||
handleWorkflowStarted,
|
||||
handleWorkflowFinished,
|
||||
handleWorkflowFailed,
|
||||
handleWorkflowNodeStarted,
|
||||
handleWorkflowNodeFinished,
|
||||
handleWorkflowNodeIterationStarted,
|
||||
handleWorkflowNodeIterationNext,
|
||||
handleWorkflowNodeIterationFinished,
|
||||
handleWorkflowNodeLoopStarted,
|
||||
handleWorkflowNodeLoopNext,
|
||||
handleWorkflowNodeLoopFinished,
|
||||
handleWorkflowNodeRetry,
|
||||
handleWorkflowTextChunk,
|
||||
handleWorkflowTextReplace,
|
||||
handleWorkflowAgentLog,
|
||||
pathname],
|
||||
)
|
||||
|
||||
const handleStopRun = useCallback((taskId: string) => {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
|
||||
stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
|
||||
}, [])
|
||||
|
||||
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
||||
const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } }))
|
||||
const edges = publishedWorkflow.graph.edges
|
||||
const viewport = publishedWorkflow.graph.viewport!
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
const mappedFeatures = {
|
||||
opening: {
|
||||
enabled: !!publishedWorkflow.features.opening_statement || !!publishedWorkflow.features.suggested_questions.length,
|
||||
opening_statement: publishedWorkflow.features.opening_statement,
|
||||
suggested_questions: publishedWorkflow.features.suggested_questions,
|
||||
},
|
||||
suggested: publishedWorkflow.features.suggested_questions_after_answer,
|
||||
text2speech: publishedWorkflow.features.text_to_speech,
|
||||
speech2text: publishedWorkflow.features.speech_to_text,
|
||||
citation: publishedWorkflow.features.retriever_resource,
|
||||
moderation: publishedWorkflow.features.sensitive_word_avoidance,
|
||||
file: publishedWorkflow.features.file_upload,
|
||||
}
|
||||
|
||||
featuresStore?.setState({ features: mappedFeatures })
|
||||
workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
|
||||
}, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
return {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
BlockEnum,
|
||||
WorkflowRunningStatus,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useWorkflowInteractions } from '@/app/components/workflow/hooks'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowRun,
|
||||
} from '.'
|
||||
|
||||
export const useWorkflowStartRun = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const { handleRun } = useWorkflowRun()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleWorkflowStartRunInWorkflow = useCallback(async () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
|
||||
return
|
||||
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables || []
|
||||
const fileSettings = featuresStore!.getState().features.file
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setShowInputsPanel,
|
||||
setShowEnvPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setShowEnvPanel(false)
|
||||
|
||||
if (showDebugAndPreviewPanel) {
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
return
|
||||
}
|
||||
|
||||
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
||||
await doSyncWorkflowDraft()
|
||||
handleRun({ inputs: {}, files: [] })
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(false)
|
||||
}
|
||||
else {
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(true)
|
||||
}
|
||||
}, [store, workflowStore, featuresStore, handleCancelDebugAndPreviewPanel, handleRun, doSyncWorkflowDraft])
|
||||
|
||||
const handleWorkflowStartRunInChatflow = useCallback(async () => {
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setHistoryWorkflowData,
|
||||
setShowEnvPanel,
|
||||
setShowChatVariablePanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setShowEnvPanel(false)
|
||||
setShowChatVariablePanel(false)
|
||||
|
||||
if (showDebugAndPreviewPanel)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
else
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
|
||||
setHistoryWorkflowData(undefined)
|
||||
}, [workflowStore, handleCancelDebugAndPreviewPanel])
|
||||
|
||||
const handleStartWorkflowRun = useCallback(() => {
|
||||
if (!isChatMode)
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
else
|
||||
handleWorkflowStartRunInChatflow()
|
||||
}, [isChatMode, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow])
|
||||
|
||||
return {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { generateNewNode } from '../utils'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import {
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '../constants'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useNodesInitialData } from './use-nodes-data'
|
||||
} from '@/app/components/workflow/constants'
|
||||
import { useNodesInitialData } from '@/app/components/workflow/hooks'
|
||||
import { useIsChatMode } from './use-is-chat-mode'
|
||||
|
||||
export const useWorkflowTemplate = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
@ -0,0 +1,108 @@
|
||||
import {
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
SupportUploadFileTypes,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
useWorkflowInit,
|
||||
} from './hooks'
|
||||
import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
import {
|
||||
WorkflowContextProvider,
|
||||
} from '@/app/components/workflow/context'
|
||||
import { createWorkflowSlice } from './store/workflow/workflow-slice'
|
||||
import WorkflowAppMain from './components/workflow-main'
|
||||
|
||||
const WorkflowAppWithAdditionalContext = () => {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
} = useWorkflowInit()
|
||||
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
|
||||
|
||||
const nodesData = useMemo(() => {
|
||||
if (data)
|
||||
return initialNodes(data.graph.nodes, data.graph.edges)
|
||||
|
||||
return []
|
||||
}, [data])
|
||||
const edgesData = useMemo(() => {
|
||||
if (data)
|
||||
return initialEdges(data.graph.edges, data.graph.nodes)
|
||||
|
||||
return []
|
||||
}, [data])
|
||||
|
||||
if (!data || isLoading) {
|
||||
return (
|
||||
<div className='relative flex h-full w-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const features = data.features || {}
|
||||
const initialFeatures: FeaturesData = {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!features.file_upload?.image?.enabled,
|
||||
number_limits: features.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
|
||||
fileUploadConfig: fileUploadConfigResponse,
|
||||
},
|
||||
opening: {
|
||||
enabled: !!features.opening_statement,
|
||||
opening_statement: features.opening_statement,
|
||||
suggested_questions: features.suggested_questions,
|
||||
},
|
||||
suggested: features.suggested_questions_after_answer || { enabled: false },
|
||||
speech2text: features.speech_to_text || { enabled: false },
|
||||
text2speech: features.text_to_speech || { enabled: false },
|
||||
citation: features.retriever_resource || { enabled: false },
|
||||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowWithDefaultContext
|
||||
edges={edgesData}
|
||||
nodes={nodesData}
|
||||
>
|
||||
<FeaturesProvider features={initialFeatures}>
|
||||
<WorkflowAppMain
|
||||
nodes={nodesData}
|
||||
edges={edgesData}
|
||||
viewport={data.graph.viewport}
|
||||
/>
|
||||
</FeaturesProvider>
|
||||
</WorkflowWithDefaultContext>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkflowAppWrapper = () => {
|
||||
return (
|
||||
<WorkflowContextProvider
|
||||
injectWorkflowStoreSliceFn={createWorkflowSlice}
|
||||
>
|
||||
<WorkflowAppWithAdditionalContext />
|
||||
</WorkflowContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowAppWrapper
|
||||
@ -0,0 +1,18 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
|
||||
export type WorkflowSliceShape = {
|
||||
appId: string
|
||||
notInitialWorkflow: boolean
|
||||
setNotInitialWorkflow: (notInitialWorkflow: boolean) => void
|
||||
nodesDefaultConfigs: Record<string, any>
|
||||
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
|
||||
}
|
||||
|
||||
export type CreateWorkflowSlice = StateCreator<WorkflowSliceShape>
|
||||
export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
||||
appId: '',
|
||||
notInitialWorkflow: false,
|
||||
setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })),
|
||||
nodesDefaultConfigs: {},
|
||||
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
|
||||
})
|
||||
@ -0,0 +1,69 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import type { StartNodeType } from '../nodes/start/types'
|
||||
import {
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import RunAndHistory from './run-and-history'
|
||||
import EditingTitle from './editing-title'
|
||||
import EnvButton from './env-button'
|
||||
import VersionHistoryButton from './version-history-button'
|
||||
|
||||
export type HeaderInNormalProps = {
|
||||
components?: {
|
||||
left?: React.ReactNode
|
||||
middle?: React.ReactNode
|
||||
}
|
||||
}
|
||||
const HeaderInNormal = ({
|
||||
components,
|
||||
}: HeaderInNormalProps) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||
const nodes = useNodes<StartNodeType>()
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
const { handleBackupDraft } = useWorkflowRun()
|
||||
|
||||
const onStartRestoring = useCallback(() => {
|
||||
workflowStore.setState({ isRestoring: true })
|
||||
handleBackupDraft()
|
||||
// clear right panel
|
||||
if (selectedNode)
|
||||
handleNodeSelect(selectedNode.id, true)
|
||||
setShowWorkflowVersionHistoryPanel(true)
|
||||
setShowEnvPanel(false)
|
||||
setShowDebugAndPreviewPanel(false)
|
||||
}, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode,
|
||||
setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<EditingTitle />
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{components?.left}
|
||||
<EnvButton disabled={nodesReadOnly} />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<RunAndHistory />
|
||||
{components?.middle}
|
||||
<VersionHistoryButton onClick={onStartRestoring} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderInNormal
|
||||
@ -0,0 +1,93 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { RiHistoryLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
WorkflowVersion,
|
||||
} from '../types'
|
||||
import {
|
||||
useNodesSyncDraft,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Toast from '../../base/toast'
|
||||
import RestoringTitle from './restoring-title'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type HeaderInRestoringProps = {
|
||||
onRestoreSettled?: () => void
|
||||
}
|
||||
const HeaderInRestoring = ({
|
||||
onRestoreSettled,
|
||||
}: HeaderInRestoringProps) => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const currentVersion = useStore(s => s.currentVersion)
|
||||
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
|
||||
|
||||
const {
|
||||
handleLoadBackupDraft,
|
||||
} = useWorkflowRun()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleCancelRestore = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
}, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel])
|
||||
|
||||
const handleRestore = useCallback(() => {
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
workflowStore.setState({ backupDraft: undefined })
|
||||
handleSyncWorkflowDraft(true, false, {
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('workflow.versionHistory.action.restoreSuccess'),
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('workflow.versionHistory.action.restoreFailure'),
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
onRestoreSettled?.()
|
||||
},
|
||||
})
|
||||
}, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, onRestoreSettled, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<RestoringTitle />
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-x-2'>
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>
|
||||
<Button
|
||||
className='text-components-button-secondary-accent-text'
|
||||
onClick={handleCancelRestore}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderInRestoring
|
||||
@ -0,0 +1,50 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import RunningTitle from './running-title'
|
||||
import ViewHistory from './view-history'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const HeaderInHistory = () => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const {
|
||||
handleLoadBackupDraft,
|
||||
} = useWorkflowRun()
|
||||
|
||||
const handleGoBackToEdit = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
}, [workflowStore, handleLoadBackupDraft])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<RunningTitle />
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<ViewHistory withText />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleGoBackToEdit}
|
||||
>
|
||||
<ArrowNarrowLeft className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.goBackToEdit')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderInHistory
|
||||
@ -1,292 +1,51 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { RiApps2AddLine, RiHistoryLine } from '@remixicon/react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
BlockEnum,
|
||||
InputVarType,
|
||||
WorkflowVersion,
|
||||
} from '../types'
|
||||
import type { StartNodeType } from '../nodes/start/types'
|
||||
import {
|
||||
useChecklistBeforePublish,
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowMode,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import AppPublisher from '../../app/app-publisher'
|
||||
import Toast, { ToastContext } from '../../base/toast'
|
||||
import Divider from '../../base/divider'
|
||||
import RunAndHistory from './run-and-history'
|
||||
import EditingTitle from './editing-title'
|
||||
import RunningTitle from './running-title'
|
||||
import RestoringTitle from './restoring-title'
|
||||
import ViewHistory from './view-history'
|
||||
import ChatVariableButton from './chat-variable-button'
|
||||
import EnvButton from './env-button'
|
||||
import VersionHistoryButton from './version-history-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
||||
import AppContext from '@/context/app-context'
|
||||
|
||||
const Header: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
||||
const appID = appDetail?.id
|
||||
const isChatMode = useIsChatMode()
|
||||
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const toolPublished = useStore(s => s.toolPublished)
|
||||
const currentVersion = useStore(s => s.currentVersion)
|
||||
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||
const nodes = useNodes<StartNodeType>()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
const startVariables = startNode?.data.variables
|
||||
const fileSettings = useFeatures(s => s.features.file)
|
||||
const variables = useMemo(() => {
|
||||
const data = startVariables || []
|
||||
if (fileSettings?.image?.enabled) {
|
||||
return [
|
||||
...data,
|
||||
{
|
||||
type: InputVarType.files,
|
||||
variable: '__image',
|
||||
required: false,
|
||||
label: 'files',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return data
|
||||
}, [fileSettings?.image?.enabled, startVariables])
|
||||
|
||||
const {
|
||||
handleLoadBackupDraft,
|
||||
handleBackupDraft,
|
||||
} = useWorkflowRun()
|
||||
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { notify } = useContext(ToastContext)
|
||||
import type { HeaderInNormalProps } from './header-in-normal'
|
||||
import HeaderInNormal from './header-in-normal'
|
||||
import HeaderInHistory from './header-in-view-history'
|
||||
import type { HeaderInRestoringProps } from './header-in-restoring'
|
||||
import HeaderInRestoring from './header-in-restoring'
|
||||
|
||||
export type HeaderProps = {
|
||||
normal?: HeaderInNormalProps
|
||||
restoring?: HeaderInRestoringProps
|
||||
}
|
||||
const Header = ({
|
||||
normal: normalProps,
|
||||
restoring: restoringProps,
|
||||
}: HeaderProps) => {
|
||||
const {
|
||||
normal,
|
||||
restoring,
|
||||
viewHistory,
|
||||
} = useWorkflowMode()
|
||||
|
||||
const handleShowFeatures = useCallback(() => {
|
||||
const {
|
||||
showFeaturesPanel,
|
||||
isRestoring,
|
||||
setShowFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
if (getNodesReadOnly() && !isRestoring)
|
||||
return
|
||||
setShowFeaturesPanel(!showFeaturesPanel)
|
||||
}, [workflowStore, getNodesReadOnly])
|
||||
|
||||
const handleCancelRestore = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
}, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel])
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)
|
||||
|
||||
const handleRestore = useCallback(() => {
|
||||
setShowWorkflowVersionHistoryPanel(false)
|
||||
workflowStore.setState({ isRestoring: false })
|
||||
workflowStore.setState({ backupDraft: undefined })
|
||||
handleSyncWorkflowDraft(true, false, {
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('workflow.versionHistory.action.restoreSuccess'),
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('workflow.versionHistory.action.restoreFailure'),
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
resetWorkflowVersionHistory()
|
||||
},
|
||||
})
|
||||
}, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, resetWorkflowVersionHistory, t])
|
||||
|
||||
const updateAppDetail = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchAppDetail({ url: '/apps', id: appID! })
|
||||
if (systemFeatures.enable_web_sso_switch_component) {
|
||||
const ssoRes = await fetchAppSSO({ appId: appID! })
|
||||
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
||||
}
|
||||
else {
|
||||
setAppDetail({ ...res })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component])
|
||||
|
||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
|
||||
|
||||
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
|
||||
if (await handleCheckBeforePublish()) {
|
||||
const res = await publishWorkflow({
|
||||
title: params?.title || '',
|
||||
releaseNotes: params?.releaseNotes || '',
|
||||
})
|
||||
|
||||
if (res) {
|
||||
notify({ type: 'success', message: t('common.api.actionSuccess') })
|
||||
updateAppDetail()
|
||||
workflowStore.getState().setPublishedAt(res.created_at)
|
||||
resetWorkflowVersionHistory()
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error('Checklist failed')
|
||||
}
|
||||
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
|
||||
|
||||
const onStartRestoring = useCallback(() => {
|
||||
workflowStore.setState({ isRestoring: true })
|
||||
handleBackupDraft()
|
||||
// clear right panel
|
||||
if (selectedNode)
|
||||
handleNodeSelect(selectedNode.id, true)
|
||||
setShowWorkflowVersionHistoryPanel(true)
|
||||
setShowEnvPanel(false)
|
||||
setShowDebugAndPreviewPanel(false)
|
||||
}, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode,
|
||||
setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel])
|
||||
|
||||
const onPublisherToggle = useCallback((state: boolean) => {
|
||||
if (state)
|
||||
handleSyncWorkflowDraft(true)
|
||||
}, [handleSyncWorkflowDraft])
|
||||
|
||||
const handleGoBackToEdit = useCallback(() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
}, [workflowStore, handleLoadBackupDraft])
|
||||
|
||||
const handleToolConfigureUpdate = useCallback(() => {
|
||||
workflowStore.setState({ toolPublished: true })
|
||||
}, [workflowStore])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||
>
|
||||
<div>
|
||||
{
|
||||
normal && <EditingTitle />
|
||||
}
|
||||
{
|
||||
viewHistory && <RunningTitle />
|
||||
}
|
||||
{
|
||||
restoring && <RestoringTitle />
|
||||
}
|
||||
</div>
|
||||
{
|
||||
normal && (
|
||||
<div className='flex items-center gap-2'>
|
||||
{/* <GlobalVariableButton disabled={nodesReadOnly} /> */}
|
||||
{isChatMode && <ChatVariableButton disabled={nodesReadOnly} />}
|
||||
<EnvButton disabled={nodesReadOnly} />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<RunAndHistory />
|
||||
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
||||
<RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
<AppPublisher
|
||||
{...{
|
||||
publishedAt,
|
||||
draftUpdatedAt,
|
||||
disabled: nodesReadOnly,
|
||||
toolPublished,
|
||||
inputs: variables,
|
||||
onRefreshData: handleToolConfigureUpdate,
|
||||
onPublish,
|
||||
onToggle: onPublisherToggle,
|
||||
crossAxisOffset: 4,
|
||||
}}
|
||||
/>
|
||||
<VersionHistoryButton onClick={onStartRestoring} />
|
||||
</div>
|
||||
<HeaderInNormal
|
||||
{...normalProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
viewHistory && (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<ViewHistory withText />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleGoBackToEdit}
|
||||
>
|
||||
<ArrowNarrowLeft className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.goBackToEdit')}
|
||||
</Button>
|
||||
</div>
|
||||
<HeaderInHistory />
|
||||
)
|
||||
}
|
||||
{
|
||||
restoring && (
|
||||
<div className='flex items-center justify-end gap-x-2'>
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>
|
||||
<Button
|
||||
className='text-components-button-secondary-accent-text'
|
||||
onClick={handleCancelRestore}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<HeaderInRestoring
|
||||
{...restoringProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Header)
|
||||
export default Header
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './provider'
|
||||
export * from './store'
|
||||
@ -0,0 +1,36 @@
|
||||
import {
|
||||
createContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useStore } from 'reactflow'
|
||||
import {
|
||||
createHooksStore,
|
||||
} from './store'
|
||||
import type { Shape } from './store'
|
||||
|
||||
type HooksStore = ReturnType<typeof createHooksStore>
|
||||
export const HooksStoreContext = createContext<HooksStore | null | undefined>(null)
|
||||
type HooksStoreContextProviderProps = Partial<Shape> & {
|
||||
children: React.ReactNode
|
||||
}
|
||||
export const HooksStoreContextProvider = ({ children, ...restProps }: HooksStoreContextProviderProps) => {
|
||||
const storeRef = useRef<HooksStore | undefined>(undefined)
|
||||
const d3Selection = useStore(s => s.d3Selection)
|
||||
const d3Zoom = useStore(s => s.d3Zoom)
|
||||
|
||||
useEffect(() => {
|
||||
if (storeRef.current && d3Selection && d3Zoom)
|
||||
storeRef.current.getState().refreshAll(restProps)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [d3Selection, d3Zoom])
|
||||
|
||||
if (!storeRef.current)
|
||||
storeRef.current = createHooksStore(restProps)
|
||||
|
||||
return (
|
||||
<HooksStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</HooksStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import { useContext } from 'react'
|
||||
import {
|
||||
noop,
|
||||
} from 'lodash-es'
|
||||
import {
|
||||
useStore as useZustandStore,
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { HooksStoreContext } from './provider'
|
||||
|
||||
type CommonHooksFnMap = {
|
||||
doSyncWorkflowDraft: (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
}
|
||||
) => Promise<void>
|
||||
syncWorkflowDraftWhenPageClose: () => void
|
||||
handleBackupDraft: () => void
|
||||
handleLoadBackupDraft: () => void
|
||||
handleRestoreFromPublishedWorkflow: (...args: any[]) => void
|
||||
handleRun: (...args: any[]) => void
|
||||
handleStopRun: (...args: any[]) => void
|
||||
handleStartWorkflowRun: () => void
|
||||
handleWorkflowStartRunInWorkflow: () => void
|
||||
handleWorkflowStartRunInChatflow: () => void
|
||||
}
|
||||
|
||||
export type Shape = {
|
||||
refreshAll: (props: Partial<CommonHooksFnMap>) => void
|
||||
} & CommonHooksFnMap
|
||||
|
||||
export const createHooksStore = ({
|
||||
doSyncWorkflowDraft = async () => noop(),
|
||||
syncWorkflowDraftWhenPageClose = noop,
|
||||
handleBackupDraft = noop,
|
||||
handleLoadBackupDraft = noop,
|
||||
handleRestoreFromPublishedWorkflow = noop,
|
||||
handleRun = noop,
|
||||
handleStopRun = noop,
|
||||
handleStartWorkflowRun = noop,
|
||||
handleWorkflowStartRunInWorkflow = noop,
|
||||
handleWorkflowStartRunInChatflow = noop,
|
||||
}: Partial<Shape>) => {
|
||||
return createStore<Shape>(set => ({
|
||||
refreshAll: props => set(state => ({ ...state, ...props })),
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
}))
|
||||
}
|
||||
|
||||
export function useHooksStore<T>(selector: (state: Shape) => T): T {
|
||||
const store = useContext(HooksStoreContext)
|
||||
if (!store)
|
||||
throw new Error('Missing HooksStoreContext.Provider in the tree')
|
||||
|
||||
return useZustandStore(store, selector)
|
||||
}
|
||||
|
||||
export const useHooksStoreApi = () => {
|
||||
return useContext(HooksStoreContext)!
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
export const useEdgesInteractionsWithoutSync = () => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleEdgeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data._sourceRunningStatus = undefined
|
||||
edge.data._targetRunningStatus = undefined
|
||||
edge.data._waitingRun = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleEdgeCancelRunningStatus,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback } from 'react'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
|
||||
export const useFormatTimeFromNow = () => {
|
||||
const { locale } = useI18N()
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
||||
}, [locale])
|
||||
|
||||
return { formatTimeFromNow }
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
export const useNodesInteractionsWithoutSync = () => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = undefined
|
||||
node.data._waitingRun = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleNodeCancelRunningStatus,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue