Signed-off-by: -LAN- <laipz8200@outlook.com>tags/1.4.2
| from core.file import FILE_MODEL_IDENTITY, File | from core.file import FILE_MODEL_IDENTITY, File | ||||
| from core.tools.tool_manager import ToolManager | from core.tools.tool_manager import ToolManager | ||||
| from core.workflow.entities.workflow_execution import WorkflowExecution | from core.workflow.entities.workflow_execution import WorkflowExecution | ||||
| from core.workflow.entities.workflow_node_execution import NodeExecution, WorkflowNodeExecutionStatus | |||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution, WorkflowNodeExecutionStatus | |||||
| from core.workflow.nodes import NodeType | from core.workflow.nodes import NodeType | ||||
| from core.workflow.nodes.tool.entities import ToolNodeData | from core.workflow.nodes.tool.entities import ToolNodeData | ||||
| from models import ( | from models import ( | ||||
| *, | *, | ||||
| event: QueueNodeStartedEvent, | event: QueueNodeStartedEvent, | ||||
| task_id: str, | task_id: str, | ||||
| workflow_node_execution: NodeExecution, | |||||
| workflow_node_execution: WorkflowNodeExecution, | |||||
| ) -> Optional[NodeStartStreamResponse]: | ) -> Optional[NodeStartStreamResponse]: | ||||
| if workflow_node_execution.node_type in {NodeType.ITERATION, NodeType.LOOP}: | if workflow_node_execution.node_type in {NodeType.ITERATION, NodeType.LOOP}: | ||||
| return None | return None | ||||
| | QueueNodeInLoopFailedEvent | | QueueNodeInLoopFailedEvent | ||||
| | QueueNodeExceptionEvent, | | QueueNodeExceptionEvent, | ||||
| task_id: str, | task_id: str, | ||||
| workflow_node_execution: NodeExecution, | |||||
| workflow_node_execution: WorkflowNodeExecution, | |||||
| ) -> Optional[NodeFinishStreamResponse]: | ) -> Optional[NodeFinishStreamResponse]: | ||||
| if workflow_node_execution.node_type in {NodeType.ITERATION, NodeType.LOOP}: | if workflow_node_execution.node_type in {NodeType.ITERATION, NodeType.LOOP}: | ||||
| return None | return None | ||||
| *, | *, | ||||
| event: QueueNodeRetryEvent, | event: QueueNodeRetryEvent, | ||||
| task_id: str, | task_id: str, | ||||
| workflow_node_execution: NodeExecution, | |||||
| workflow_node_execution: WorkflowNodeExecution, | |||||
| ) -> Optional[Union[NodeRetryStreamResponse, NodeFinishStreamResponse]]: | ) -> Optional[Union[NodeRetryStreamResponse, NodeFinishStreamResponse]]: | ||||
| if workflow_node_execution.node_type in {NodeType.ITERATION, NodeType.LOOP}: | if workflow_node_execution.node_type in {NodeType.ITERATION, NodeType.LOOP}: | ||||
| return None | return None |
| from core.model_runtime.utils.encoders import jsonable_encoder | from core.model_runtime.utils.encoders import jsonable_encoder | ||||
| from core.workflow.entities.workflow_node_execution import ( | from core.workflow.entities.workflow_node_execution import ( | ||||
| NodeExecution, | |||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionMetadataKey, | WorkflowNodeExecutionMetadataKey, | ||||
| WorkflowNodeExecutionStatus, | WorkflowNodeExecutionStatus, | ||||
| ) | ) | ||||
| Account, | Account, | ||||
| CreatorUserRole, | CreatorUserRole, | ||||
| EndUser, | EndUser, | ||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionModel, | |||||
| WorkflowNodeExecutionTriggeredFrom, | WorkflowNodeExecutionTriggeredFrom, | ||||
| ) | ) | ||||
| # Initialize in-memory cache for node executions | # Initialize in-memory cache for node executions | ||||
| # Key: node_execution_id, Value: WorkflowNodeExecution (DB model) | # Key: node_execution_id, Value: WorkflowNodeExecution (DB model) | ||||
| self._node_execution_cache: dict[str, WorkflowNodeExecution] = {} | |||||
| self._node_execution_cache: dict[str, WorkflowNodeExecutionModel] = {} | |||||
| def _to_domain_model(self, db_model: WorkflowNodeExecution) -> NodeExecution: | |||||
| def _to_domain_model(self, db_model: WorkflowNodeExecutionModel) -> WorkflowNodeExecution: | |||||
| """ | """ | ||||
| Convert a database model to a domain model. | Convert a database model to a domain model. | ||||
| # Convert status to domain enum | # Convert status to domain enum | ||||
| status = WorkflowNodeExecutionStatus(db_model.status) | status = WorkflowNodeExecutionStatus(db_model.status) | ||||
| return NodeExecution( | |||||
| return WorkflowNodeExecution( | |||||
| id=db_model.id, | id=db_model.id, | ||||
| node_execution_id=db_model.node_execution_id, | node_execution_id=db_model.node_execution_id, | ||||
| workflow_id=db_model.workflow_id, | workflow_id=db_model.workflow_id, | ||||
| finished_at=db_model.finished_at, | finished_at=db_model.finished_at, | ||||
| ) | ) | ||||
| def to_db_model(self, domain_model: NodeExecution) -> WorkflowNodeExecution: | |||||
| def to_db_model(self, domain_model: WorkflowNodeExecution) -> WorkflowNodeExecutionModel: | |||||
| """ | """ | ||||
| Convert a domain model to a database model. | Convert a domain model to a database model. | ||||
| if not self._creator_user_role: | if not self._creator_user_role: | ||||
| raise ValueError("created_by_role is required in repository constructor") | raise ValueError("created_by_role is required in repository constructor") | ||||
| db_model = WorkflowNodeExecution() | |||||
| db_model = WorkflowNodeExecutionModel() | |||||
| db_model.id = domain_model.id | db_model.id = domain_model.id | ||||
| db_model.tenant_id = self._tenant_id | db_model.tenant_id = self._tenant_id | ||||
| if self._app_id is not None: | if self._app_id is not None: | ||||
| db_model.finished_at = domain_model.finished_at | db_model.finished_at = domain_model.finished_at | ||||
| return db_model | return db_model | ||||
| def save(self, execution: NodeExecution) -> None: | |||||
| def save(self, execution: WorkflowNodeExecution) -> None: | |||||
| """ | """ | ||||
| Save or update a NodeExecution domain entity to the database. | Save or update a NodeExecution domain entity to the database. | ||||
| logger.debug(f"Updating cache for node_execution_id: {db_model.node_execution_id}") | logger.debug(f"Updating cache for node_execution_id: {db_model.node_execution_id}") | ||||
| self._node_execution_cache[db_model.node_execution_id] = db_model | self._node_execution_cache[db_model.node_execution_id] = db_model | ||||
| def get_by_node_execution_id(self, node_execution_id: str) -> Optional[NodeExecution]: | |||||
| def get_by_node_execution_id(self, node_execution_id: str) -> Optional[WorkflowNodeExecution]: | |||||
| """ | """ | ||||
| Retrieve a NodeExecution by its node_execution_id. | Retrieve a NodeExecution by its node_execution_id. | ||||
| # If not in cache, query the database | # If not in cache, query the database | ||||
| logger.debug(f"Cache miss for node_execution_id: {node_execution_id}, querying database") | logger.debug(f"Cache miss for node_execution_id: {node_execution_id}, querying database") | ||||
| with self._session_factory() as session: | with self._session_factory() as session: | ||||
| stmt = select(WorkflowNodeExecution).where( | |||||
| WorkflowNodeExecution.node_execution_id == node_execution_id, | |||||
| WorkflowNodeExecution.tenant_id == self._tenant_id, | |||||
| stmt = select(WorkflowNodeExecutionModel).where( | |||||
| WorkflowNodeExecutionModel.node_execution_id == node_execution_id, | |||||
| WorkflowNodeExecutionModel.tenant_id == self._tenant_id, | |||||
| ) | ) | ||||
| if self._app_id: | if self._app_id: | ||||
| stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id) | |||||
| stmt = stmt.where(WorkflowNodeExecutionModel.app_id == self._app_id) | |||||
| db_model = session.scalar(stmt) | db_model = session.scalar(stmt) | ||||
| if db_model: | if db_model: | ||||
| self, | self, | ||||
| workflow_run_id: str, | workflow_run_id: str, | ||||
| order_config: Optional[OrderConfig] = None, | order_config: Optional[OrderConfig] = None, | ||||
| ) -> Sequence[WorkflowNodeExecution]: | |||||
| ) -> Sequence[WorkflowNodeExecutionModel]: | |||||
| """ | """ | ||||
| Retrieve all WorkflowNodeExecution database models for a specific workflow run. | Retrieve all WorkflowNodeExecution database models for a specific workflow run. | ||||
| A list of WorkflowNodeExecution database models | A list of WorkflowNodeExecution database models | ||||
| """ | """ | ||||
| with self._session_factory() as session: | 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, | |||||
| stmt = select(WorkflowNodeExecutionModel).where( | |||||
| WorkflowNodeExecutionModel.workflow_run_id == workflow_run_id, | |||||
| WorkflowNodeExecutionModel.tenant_id == self._tenant_id, | |||||
| WorkflowNodeExecutionModel.triggered_from == WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | |||||
| ) | ) | ||||
| if self._app_id: | if self._app_id: | ||||
| stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id) | |||||
| stmt = stmt.where(WorkflowNodeExecutionModel.app_id == self._app_id) | |||||
| # Apply ordering if provided | # Apply ordering if provided | ||||
| if order_config and order_config.order_by: | if order_config and order_config.order_by: | ||||
| order_columns: list[UnaryExpression] = [] | order_columns: list[UnaryExpression] = [] | ||||
| for field in order_config.order_by: | for field in order_config.order_by: | ||||
| column = getattr(WorkflowNodeExecution, field, None) | |||||
| column = getattr(WorkflowNodeExecutionModel, field, None) | |||||
| if not column: | if not column: | ||||
| continue | continue | ||||
| if order_config.order_direction == "desc": | if order_config.order_direction == "desc": | ||||
| self, | self, | ||||
| workflow_run_id: str, | workflow_run_id: str, | ||||
| order_config: Optional[OrderConfig] = None, | order_config: Optional[OrderConfig] = None, | ||||
| ) -> Sequence[NodeExecution]: | |||||
| ) -> Sequence[WorkflowNodeExecution]: | |||||
| """ | """ | ||||
| Retrieve all NodeExecution instances for a specific workflow run. | Retrieve all NodeExecution instances for a specific workflow run. | ||||
| return domain_models | return domain_models | ||||
| def get_running_executions(self, workflow_run_id: str) -> Sequence[NodeExecution]: | |||||
| def get_running_executions(self, workflow_run_id: str) -> Sequence[WorkflowNodeExecution]: | |||||
| """ | """ | ||||
| Retrieve all running NodeExecution instances for a specific workflow run. | Retrieve all running NodeExecution instances for a specific workflow run. | ||||
| A list of running NodeExecution instances | A list of running NodeExecution instances | ||||
| """ | """ | ||||
| with self._session_factory() as session: | 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, | |||||
| stmt = select(WorkflowNodeExecutionModel).where( | |||||
| WorkflowNodeExecutionModel.workflow_run_id == workflow_run_id, | |||||
| WorkflowNodeExecutionModel.tenant_id == self._tenant_id, | |||||
| WorkflowNodeExecutionModel.status == WorkflowNodeExecutionStatus.RUNNING, | |||||
| WorkflowNodeExecutionModel.triggered_from == WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, | |||||
| ) | ) | ||||
| if self._app_id: | if self._app_id: | ||||
| stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id) | |||||
| stmt = stmt.where(WorkflowNodeExecutionModel.app_id == self._app_id) | |||||
| db_models = session.scalars(stmt).all() | db_models = session.scalars(stmt).all() | ||||
| domain_models = [] | domain_models = [] | ||||
| It also clears the in-memory cache. | It also clears the in-memory cache. | ||||
| """ | """ | ||||
| with self._session_factory() as session: | with self._session_factory() as session: | ||||
| stmt = delete(WorkflowNodeExecution).where(WorkflowNodeExecution.tenant_id == self._tenant_id) | |||||
| stmt = delete(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.tenant_id == self._tenant_id) | |||||
| if self._app_id: | if self._app_id: | ||||
| stmt = stmt.where(WorkflowNodeExecution.app_id == self._app_id) | |||||
| stmt = stmt.where(WorkflowNodeExecutionModel.app_id == self._app_id) | |||||
| result = session.execute(stmt) | result = session.execute(stmt) | ||||
| session.commit() | session.commit() |
| RETRY = "retry" | RETRY = "retry" | ||||
| class NodeExecution(BaseModel): | |||||
| class WorkflowNodeExecution(BaseModel): | |||||
| """ | """ | ||||
| Domain model for workflow node execution. | Domain model for workflow node execution. | ||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||
| from typing import Literal, Optional, Protocol | from typing import Literal, Optional, Protocol | ||||
| from core.workflow.entities.workflow_node_execution import NodeExecution | |||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution | |||||
| @dataclass | @dataclass | ||||
| application domains or deployment scenarios. | application domains or deployment scenarios. | ||||
| """ | """ | ||||
| def save(self, execution: NodeExecution) -> None: | |||||
| def save(self, execution: WorkflowNodeExecution) -> None: | |||||
| """ | """ | ||||
| Save or update a NodeExecution instance. | Save or update a NodeExecution instance. | ||||
| """ | """ | ||||
| ... | ... | ||||
| def get_by_node_execution_id(self, node_execution_id: str) -> Optional[NodeExecution]: | |||||
| def get_by_node_execution_id(self, node_execution_id: str) -> Optional[WorkflowNodeExecution]: | |||||
| """ | """ | ||||
| Retrieve a NodeExecution by its node_execution_id. | Retrieve a NodeExecution by its node_execution_id. | ||||
| self, | self, | ||||
| workflow_run_id: str, | workflow_run_id: str, | ||||
| order_config: Optional[OrderConfig] = None, | order_config: Optional[OrderConfig] = None, | ||||
| ) -> Sequence[NodeExecution]: | |||||
| ) -> Sequence[WorkflowNodeExecution]: | |||||
| """ | """ | ||||
| Retrieve all NodeExecution instances for a specific workflow run. | Retrieve all NodeExecution instances for a specific workflow run. | ||||
| """ | """ | ||||
| ... | ... | ||||
| def get_running_executions(self, workflow_run_id: str) -> Sequence[NodeExecution]: | |||||
| def get_running_executions(self, workflow_run_id: str) -> Sequence[WorkflowNodeExecution]: | |||||
| """ | """ | ||||
| Retrieve all running NodeExecution instances for a specific workflow run. | Retrieve all running NodeExecution instances for a specific workflow run. | ||||
| from core.ops.ops_trace_manager import TraceQueueManager, TraceTask | from core.ops.ops_trace_manager import TraceQueueManager, TraceTask | ||||
| from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType | from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType | ||||
| from core.workflow.entities.workflow_node_execution import ( | from core.workflow.entities.workflow_node_execution import ( | ||||
| NodeExecution, | |||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionMetadataKey, | WorkflowNodeExecutionMetadataKey, | ||||
| WorkflowNodeExecutionStatus, | WorkflowNodeExecutionStatus, | ||||
| ) | ) | ||||
| *, | *, | ||||
| workflow_execution_id: str, | workflow_execution_id: str, | ||||
| event: QueueNodeStartedEvent, | event: QueueNodeStartedEvent, | ||||
| ) -> NodeExecution: | |||||
| ) -> WorkflowNodeExecution: | |||||
| workflow_execution = self._get_workflow_execution_or_raise_error(workflow_execution_id) | workflow_execution = self._get_workflow_execution_or_raise_error(workflow_execution_id) | ||||
| # Create a domain model | # Create a domain model | ||||
| WorkflowNodeExecutionMetadataKey.LOOP_ID: event.in_loop_id, | WorkflowNodeExecutionMetadataKey.LOOP_ID: event.in_loop_id, | ||||
| } | } | ||||
| domain_execution = NodeExecution( | |||||
| domain_execution = WorkflowNodeExecution( | |||||
| id=str(uuid4()), | id=str(uuid4()), | ||||
| workflow_id=workflow_execution.workflow_id, | workflow_id=workflow_execution.workflow_id, | ||||
| workflow_execution_id=workflow_execution.id_, | workflow_execution_id=workflow_execution.id_, | ||||
| return domain_execution | return domain_execution | ||||
| def handle_workflow_node_execution_success(self, *, event: QueueNodeSucceededEvent) -> NodeExecution: | |||||
| def handle_workflow_node_execution_success(self, *, event: QueueNodeSucceededEvent) -> WorkflowNodeExecution: | |||||
| # Get the domain model from repository | # Get the domain model from repository | ||||
| domain_execution = self._workflow_node_execution_repository.get_by_node_execution_id(event.node_execution_id) | domain_execution = self._workflow_node_execution_repository.get_by_node_execution_id(event.node_execution_id) | ||||
| if not domain_execution: | if not domain_execution: | ||||
| | QueueNodeInIterationFailedEvent | | QueueNodeInIterationFailedEvent | ||||
| | QueueNodeInLoopFailedEvent | | QueueNodeInLoopFailedEvent | ||||
| | QueueNodeExceptionEvent, | | QueueNodeExceptionEvent, | ||||
| ) -> NodeExecution: | |||||
| ) -> WorkflowNodeExecution: | |||||
| """ | """ | ||||
| Workflow node execution failed | Workflow node execution failed | ||||
| :param event: queue node failed event | :param event: queue node failed event | ||||
| def handle_workflow_node_execution_retried( | def handle_workflow_node_execution_retried( | ||||
| self, *, workflow_execution_id: str, event: QueueNodeRetryEvent | self, *, workflow_execution_id: str, event: QueueNodeRetryEvent | ||||
| ) -> NodeExecution: | |||||
| ) -> WorkflowNodeExecution: | |||||
| workflow_execution = self._get_workflow_execution_or_raise_error(workflow_execution_id) | workflow_execution = self._get_workflow_execution_or_raise_error(workflow_execution_id) | ||||
| created_at = event.start_at | created_at = event.start_at | ||||
| finished_at = datetime.now(UTC).replace(tzinfo=None) | finished_at = datetime.now(UTC).replace(tzinfo=None) | ||||
| merged_metadata = {**execution_metadata_dict, **origin_metadata} if execution_metadata_dict else origin_metadata | merged_metadata = {**execution_metadata_dict, **origin_metadata} if execution_metadata_dict else origin_metadata | ||||
| # Create a domain model | # Create a domain model | ||||
| domain_execution = NodeExecution( | |||||
| domain_execution = WorkflowNodeExecution( | |||||
| id=str(uuid4()), | id=str(uuid4()), | ||||
| workflow_id=workflow_execution.workflow_id, | workflow_id=workflow_execution.workflow_id, | ||||
| workflow_execution_id=workflow_execution.id_, | workflow_execution_id=workflow_execution.id_, |
| Workflow, | Workflow, | ||||
| WorkflowAppLog, | WorkflowAppLog, | ||||
| WorkflowAppLogCreatedFrom, | WorkflowAppLogCreatedFrom, | ||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionModel, | |||||
| WorkflowNodeExecutionTriggeredFrom, | WorkflowNodeExecutionTriggeredFrom, | ||||
| WorkflowRun, | WorkflowRun, | ||||
| WorkflowType, | WorkflowType, | ||||
| "Workflow", | "Workflow", | ||||
| "WorkflowAppLog", | "WorkflowAppLog", | ||||
| "WorkflowAppLogCreatedFrom", | "WorkflowAppLogCreatedFrom", | ||||
| "WorkflowNodeExecution", | |||||
| "WorkflowNodeExecutionModel", | |||||
| "WorkflowNodeExecutionTriggeredFrom", | "WorkflowNodeExecutionTriggeredFrom", | ||||
| "WorkflowRun", | "WorkflowRun", | ||||
| "WorkflowRunTriggeredFrom", | "WorkflowRunTriggeredFrom", |
| WORKFLOW_RUN = "workflow-run" | WORKFLOW_RUN = "workflow-run" | ||||
| class WorkflowNodeExecution(Base): | |||||
| class WorkflowNodeExecutionModel(Base): | |||||
| """ | """ | ||||
| Workflow Node Execution | Workflow Node Execution | ||||
| from extensions.ext_storage import storage | from extensions.ext_storage import storage | ||||
| from models.account import Tenant | from models.account import Tenant | ||||
| from models.model import App, Conversation, Message | from models.model import App, Conversation, Message | ||||
| from models.workflow import WorkflowNodeExecution, WorkflowRun | |||||
| from models.workflow import WorkflowNodeExecutionModel, WorkflowRun | |||||
| from services.billing_service import BillingService | from services.billing_service import BillingService | ||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
| while True: | while True: | ||||
| with Session(db.engine).no_autoflush as session: | with Session(db.engine).no_autoflush as session: | ||||
| workflow_node_executions = ( | workflow_node_executions = ( | ||||
| session.query(WorkflowNodeExecution) | |||||
| session.query(WorkflowNodeExecutionModel) | |||||
| .filter( | .filter( | ||||
| WorkflowNodeExecution.tenant_id == tenant_id, | |||||
| WorkflowNodeExecution.created_at < datetime.datetime.now() - datetime.timedelta(days=days), | |||||
| WorkflowNodeExecutionModel.tenant_id == tenant_id, | |||||
| WorkflowNodeExecutionModel.created_at | |||||
| < datetime.datetime.now() - datetime.timedelta(days=days), | |||||
| ) | ) | ||||
| .limit(batch) | .limit(batch) | ||||
| .all() | .all() | ||||
| ] | ] | ||||
| # delete workflow node executions | # delete workflow node executions | ||||
| session.query(WorkflowNodeExecution).filter( | |||||
| WorkflowNodeExecution.id.in_(workflow_node_execution_ids), | |||||
| session.query(WorkflowNodeExecutionModel).filter( | |||||
| WorkflowNodeExecutionModel.id.in_(workflow_node_execution_ids), | |||||
| ).delete(synchronize_session=False) | ).delete(synchronize_session=False) | ||||
| session.commit() | session.commit() | ||||
| Account, | Account, | ||||
| App, | App, | ||||
| EndUser, | EndUser, | ||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionModel, | |||||
| WorkflowRun, | WorkflowRun, | ||||
| WorkflowRunTriggeredFrom, | WorkflowRunTriggeredFrom, | ||||
| ) | ) | ||||
| app_model: App, | app_model: App, | ||||
| run_id: str, | run_id: str, | ||||
| user: Account | EndUser, | user: Account | EndUser, | ||||
| ) -> Sequence[WorkflowNodeExecution]: | |||||
| ) -> Sequence[WorkflowNodeExecutionModel]: | |||||
| """ | """ | ||||
| Get workflow run node execution list | Get workflow run node execution list | ||||
| """ | """ |
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | ||||
| from core.variables import Variable | from core.variables import Variable | ||||
| from core.workflow.entities.node_entities import NodeRunResult | from core.workflow.entities.node_entities import NodeRunResult | ||||
| from core.workflow.entities.workflow_node_execution import NodeExecution, WorkflowNodeExecutionStatus | |||||
| from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution, WorkflowNodeExecutionStatus | |||||
| from core.workflow.errors import WorkflowNodeRunFailedError | from core.workflow.errors import WorkflowNodeRunFailedError | ||||
| from core.workflow.graph_engine.entities.event import InNodeEvent | from core.workflow.graph_engine.entities.event import InNodeEvent | ||||
| from core.workflow.nodes import NodeType | from core.workflow.nodes import NodeType | ||||
| from models.tools import WorkflowToolProvider | from models.tools import WorkflowToolProvider | ||||
| from models.workflow import ( | from models.workflow import ( | ||||
| Workflow, | Workflow, | ||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionModel, | |||||
| WorkflowNodeExecutionTriggeredFrom, | WorkflowNodeExecutionTriggeredFrom, | ||||
| WorkflowType, | WorkflowType, | ||||
| ) | ) | ||||
| def run_draft_workflow_node( | def run_draft_workflow_node( | ||||
| self, app_model: App, node_id: str, user_inputs: dict, account: Account | self, app_model: App, node_id: str, user_inputs: dict, account: Account | ||||
| ) -> WorkflowNodeExecution: | |||||
| ) -> WorkflowNodeExecutionModel: | |||||
| """ | """ | ||||
| Run draft workflow node | Run draft workflow node | ||||
| """ | """ | ||||
| def run_free_workflow_node( | def run_free_workflow_node( | ||||
| self, node_data: dict, tenant_id: str, user_id: str, node_id: str, user_inputs: dict[str, Any] | self, node_data: dict, tenant_id: str, user_id: str, node_id: str, user_inputs: dict[str, Any] | ||||
| ) -> NodeExecution: | |||||
| ) -> WorkflowNodeExecution: | |||||
| """ | """ | ||||
| Run draft workflow node | Run draft workflow node | ||||
| """ | """ | ||||
| invoke_node_fn: Callable[[], tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]], | invoke_node_fn: Callable[[], tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]], | ||||
| start_at: float, | start_at: float, | ||||
| node_id: str, | node_id: str, | ||||
| ) -> NodeExecution: | |||||
| ) -> WorkflowNodeExecution: | |||||
| try: | try: | ||||
| node_instance, generator = invoke_node_fn() | node_instance, generator = invoke_node_fn() | ||||
| error = e.error | error = e.error | ||||
| # Create a NodeExecution domain model | # Create a NodeExecution domain model | ||||
| node_execution = NodeExecution( | |||||
| node_execution = WorkflowNodeExecution( | |||||
| id=str(uuid4()), | id=str(uuid4()), | ||||
| workflow_id="", # This is a single-step execution, so no workflow ID | workflow_id="", # This is a single-step execution, so no workflow ID | ||||
| index=1, | index=1, |
| ) | ) | ||||
| from models.tools import WorkflowToolProvider | from models.tools import WorkflowToolProvider | ||||
| from models.web import PinnedConversation, SavedMessage | from models.web import PinnedConversation, SavedMessage | ||||
| from models.workflow import ConversationVariable, Workflow, WorkflowAppLog, WorkflowNodeExecution, WorkflowRun | |||||
| from models.workflow import ConversationVariable, Workflow, WorkflowAppLog, WorkflowNodeExecutionModel, WorkflowRun | |||||
| @shared_task(queue="app_deletion", bind=True, max_retries=3) | @shared_task(queue="app_deletion", bind=True, max_retries=3) | ||||
| def _delete_app_workflow_node_executions(tenant_id: str, app_id: str): | def _delete_app_workflow_node_executions(tenant_id: str, app_id: str): | ||||
| def del_workflow_node_execution(workflow_node_execution_id: str): | def del_workflow_node_execution(workflow_node_execution_id: str): | ||||
| db.session.query(WorkflowNodeExecution).filter(WorkflowNodeExecution.id == workflow_node_execution_id).delete( | |||||
| synchronize_session=False | |||||
| ) | |||||
| db.session.query(WorkflowNodeExecutionModel).filter( | |||||
| WorkflowNodeExecutionModel.id == workflow_node_execution_id | |||||
| ).delete(synchronize_session=False) | |||||
| _delete_records( | _delete_records( | ||||
| """select id from workflow_node_executions where tenant_id=:tenant_id and app_id=:app_id limit 1000""", | """select id from workflow_node_executions where tenant_id=:tenant_id and app_id=:app_id limit 1000""", |
| ) | ) | ||||
| from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType | from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType | ||||
| from core.workflow.entities.workflow_node_execution import ( | from core.workflow.entities.workflow_node_execution import ( | ||||
| NodeExecution, | |||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionMetadataKey, | WorkflowNodeExecutionMetadataKey, | ||||
| WorkflowNodeExecutionStatus, | WorkflowNodeExecutionStatus, | ||||
| ) | ) | ||||
| # Create a real node execution | # Create a real node execution | ||||
| node_execution = NodeExecution( | |||||
| node_execution = WorkflowNodeExecution( | |||||
| id="test-node-execution-record-id", | id="test-node-execution-record-id", | ||||
| node_execution_id="test-node-execution-id", | node_execution_id="test-node-execution-id", | ||||
| workflow_id="test-workflow-id", | workflow_id="test-workflow-id", | ||||
| # Create a real node execution | # Create a real node execution | ||||
| node_execution = NodeExecution( | |||||
| node_execution = WorkflowNodeExecution( | |||||
| id="test-node-execution-record-id", | id="test-node-execution-record-id", | ||||
| node_execution_id="test-node-execution-id", | node_execution_id="test-node-execution-id", | ||||
| workflow_id="test-workflow-id", | workflow_id="test-workflow-id", |
| from constants import HIDDEN_VALUE | from constants import HIDDEN_VALUE | ||||
| from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable | from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable | ||||
| from models.workflow import Workflow, WorkflowNodeExecution | |||||
| from models.workflow import Workflow, WorkflowNodeExecutionModel | |||||
| def test_environment_variables(): | def test_environment_variables(): | ||||
| class TestWorkflowNodeExecution: | class TestWorkflowNodeExecution: | ||||
| def test_execution_metadata_dict(self): | def test_execution_metadata_dict(self): | ||||
| node_exec = WorkflowNodeExecution() | |||||
| node_exec = WorkflowNodeExecutionModel() | |||||
| node_exec.execution_metadata = None | node_exec.execution_metadata = None | ||||
| assert node_exec.execution_metadata_dict == {} | assert node_exec.execution_metadata_dict == {} | ||||
| from core.model_runtime.utils.encoders import jsonable_encoder | from core.model_runtime.utils.encoders import jsonable_encoder | ||||
| from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository | ||||
| from core.workflow.entities.workflow_node_execution import ( | from core.workflow.entities.workflow_node_execution import ( | ||||
| NodeExecution, | |||||
| WorkflowNodeExecution, | |||||
| WorkflowNodeExecutionMetadataKey, | WorkflowNodeExecutionMetadataKey, | ||||
| WorkflowNodeExecutionStatus, | WorkflowNodeExecutionStatus, | ||||
| ) | ) | ||||
| from core.workflow.nodes.enums import NodeType | from core.workflow.nodes.enums import NodeType | ||||
| from core.workflow.repositories.workflow_node_execution_repository import OrderConfig | from core.workflow.repositories.workflow_node_execution_repository import OrderConfig | ||||
| from models.account import Account, Tenant | from models.account import Account, Tenant | ||||
| from models.workflow import WorkflowNodeExecution, WorkflowNodeExecutionTriggeredFrom | |||||
| from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom | |||||
| def configure_mock_execution(mock_execution): | def configure_mock_execution(mock_execution): | ||||
| """Test save method.""" | """Test save method.""" | ||||
| session_obj, _ = session | session_obj, _ = session | ||||
| # Create a mock execution | # Create a mock execution | ||||
| execution = MagicMock(spec=WorkflowNodeExecution) | |||||
| execution = MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| execution.tenant_id = None | execution.tenant_id = None | ||||
| execution.app_id = None | execution.app_id = None | ||||
| execution.inputs = None | execution.inputs = None | ||||
| """Test save method with existing tenant_id.""" | """Test save method with existing tenant_id.""" | ||||
| session_obj, _ = session | session_obj, _ = session | ||||
| # Create a mock execution with existing tenant_id | # Create a mock execution with existing tenant_id | ||||
| execution = MagicMock(spec=WorkflowNodeExecution) | |||||
| execution = MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| execution.tenant_id = "existing-tenant" | execution.tenant_id = "existing-tenant" | ||||
| execution.app_id = None | execution.app_id = None | ||||
| execution.inputs = None | execution.inputs = None | ||||
| execution.metadata = None | execution.metadata = None | ||||
| # Create a modified execution that will be returned by _to_db_model | # Create a modified execution that will be returned by _to_db_model | ||||
| modified_execution = MagicMock(spec=WorkflowNodeExecution) | |||||
| modified_execution = MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| modified_execution.tenant_id = "existing-tenant" # Tenant ID should not change | modified_execution.tenant_id = "existing-tenant" # Tenant ID should not change | ||||
| modified_execution.app_id = repository._app_id # App ID should be set | modified_execution.app_id = repository._app_id # App ID should be set | ||||
| mock_stmt.where.return_value = mock_stmt | mock_stmt.where.return_value = mock_stmt | ||||
| # Create a properly configured mock execution | # Create a properly configured mock execution | ||||
| mock_execution = mocker.MagicMock(spec=WorkflowNodeExecution) | |||||
| mock_execution = mocker.MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| configure_mock_execution(mock_execution) | configure_mock_execution(mock_execution) | ||||
| session_obj.scalar.return_value = mock_execution | session_obj.scalar.return_value = mock_execution | ||||
| mock_stmt.order_by.return_value = mock_stmt | mock_stmt.order_by.return_value = mock_stmt | ||||
| # Create a properly configured mock execution | # Create a properly configured mock execution | ||||
| mock_execution = mocker.MagicMock(spec=WorkflowNodeExecution) | |||||
| mock_execution = mocker.MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| configure_mock_execution(mock_execution) | configure_mock_execution(mock_execution) | ||||
| session_obj.scalars.return_value.all.return_value = [mock_execution] | session_obj.scalars.return_value.all.return_value = [mock_execution] | ||||
| mock_stmt.where.return_value = mock_stmt | mock_stmt.where.return_value = mock_stmt | ||||
| # Create a properly configured mock execution | # Create a properly configured mock execution | ||||
| mock_execution = mocker.MagicMock(spec=WorkflowNodeExecution) | |||||
| mock_execution = mocker.MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| configure_mock_execution(mock_execution) | configure_mock_execution(mock_execution) | ||||
| session_obj.scalars.return_value.all.return_value = [mock_execution] | session_obj.scalars.return_value.all.return_value = [mock_execution] | ||||
| """Test updating an existing record via save method.""" | """Test updating an existing record via save method.""" | ||||
| session_obj, _ = session | session_obj, _ = session | ||||
| # Create a mock execution | # Create a mock execution | ||||
| execution = MagicMock(spec=WorkflowNodeExecution) | |||||
| execution = MagicMock(spec=WorkflowNodeExecutionModel) | |||||
| execution.tenant_id = None | execution.tenant_id = None | ||||
| execution.app_id = None | execution.app_id = None | ||||
| execution.inputs = None | execution.inputs = None | ||||
| repository.clear() | repository.clear() | ||||
| # Assert delete was called with correct parameters | # Assert delete was called with correct parameters | ||||
| mock_delete.assert_called_once_with(WorkflowNodeExecution) | |||||
| mock_delete.assert_called_once_with(WorkflowNodeExecutionModel) | |||||
| mock_stmt.where.assert_called() | mock_stmt.where.assert_called() | ||||
| session_obj.execute.assert_called_once_with(mock_stmt) | session_obj.execute.assert_called_once_with(mock_stmt) | ||||
| session_obj.commit.assert_called_once() | session_obj.commit.assert_called_once() | ||||
| def test_to_db_model(repository): | def test_to_db_model(repository): | ||||
| """Test to_db_model method.""" | """Test to_db_model method.""" | ||||
| # Create a domain model | # Create a domain model | ||||
| domain_model = NodeExecution( | |||||
| domain_model = WorkflowNodeExecution( | |||||
| id="test-id", | id="test-id", | ||||
| workflow_id="test-workflow-id", | workflow_id="test-workflow-id", | ||||
| node_execution_id="test-node-execution-id", | node_execution_id="test-node-execution-id", | ||||
| db_model = repository.to_db_model(domain_model) | db_model = repository.to_db_model(domain_model) | ||||
| # Assert DB model has correct values | # Assert DB model has correct values | ||||
| assert isinstance(db_model, WorkflowNodeExecution) | |||||
| assert isinstance(db_model, WorkflowNodeExecutionModel) | |||||
| assert db_model.id == domain_model.id | assert db_model.id == domain_model.id | ||||
| assert db_model.tenant_id == repository._tenant_id | assert db_model.tenant_id == repository._tenant_id | ||||
| assert db_model.app_id == repository._app_id | assert db_model.app_id == repository._app_id | ||||
| metadata_dict = {str(WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS): 100} | metadata_dict = {str(WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS): 100} | ||||
| # Create a DB model using our custom subclass | # Create a DB model using our custom subclass | ||||
| db_model = WorkflowNodeExecution() | |||||
| db_model = WorkflowNodeExecutionModel() | |||||
| db_model.id = "test-id" | db_model.id = "test-id" | ||||
| db_model.tenant_id = "test-tenant-id" | db_model.tenant_id = "test-tenant-id" | ||||
| db_model.app_id = "test-app-id" | db_model.app_id = "test-app-id" | ||||
| domain_model = repository._to_domain_model(db_model) | domain_model = repository._to_domain_model(db_model) | ||||
| # Assert domain model has correct values | # Assert domain model has correct values | ||||
| assert isinstance(domain_model, NodeExecution) | |||||
| assert isinstance(domain_model, WorkflowNodeExecution) | |||||
| assert domain_model.id == db_model.id | assert domain_model.id == db_model.id | ||||
| assert domain_model.workflow_id == db_model.workflow_id | assert domain_model.workflow_id == db_model.workflow_id | ||||
| assert domain_model.workflow_execution_id == db_model.workflow_run_id | assert domain_model.workflow_execution_id == db_model.workflow_run_id |