mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-10 23:44:36 +00:00
add workflow failure reason (#1198)
This commit is contained in:
parent
b2516dc95f
commit
e3aa583b24
7 changed files with 152 additions and 25 deletions
|
@ -0,0 +1,31 @@
|
||||||
|
"""Add failure_reason column to workflow_runs
|
||||||
|
|
||||||
|
Revision ID: 1909715536dc
|
||||||
|
Revises: b8f9e09e181d
|
||||||
|
Create Date: 2024-11-15 02:51:33.553177+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "1909715536dc"
|
||||||
|
down_revision: Union[str, None] = "b8f9e09e181d"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column("workflow_runs", sa.Column("failure_reason", sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("workflow_runs", "failure_reason")
|
||||||
|
# ### end Alembic commands ###
|
|
@ -1083,13 +1083,16 @@ class AgentDB:
|
||||||
LOG.error("SQLAlchemyError", exc_info=True)
|
LOG.error("SQLAlchemyError", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def update_workflow_run(self, workflow_run_id: str, status: WorkflowRunStatus) -> WorkflowRun | None:
|
async def update_workflow_run(
|
||||||
|
self, workflow_run_id: str, status: WorkflowRunStatus, failure_reason: str | None = None
|
||||||
|
) -> WorkflowRun | None:
|
||||||
async with self.Session() as session:
|
async with self.Session() as session:
|
||||||
workflow_run = (
|
workflow_run = (
|
||||||
await session.scalars(select(WorkflowRunModel).filter_by(workflow_run_id=workflow_run_id))
|
await session.scalars(select(WorkflowRunModel).filter_by(workflow_run_id=workflow_run_id))
|
||||||
).first()
|
).first()
|
||||||
if workflow_run:
|
if workflow_run:
|
||||||
workflow_run.status = status
|
workflow_run.status = status
|
||||||
|
workflow_run.failure_reason = failure_reason
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(workflow_run)
|
await session.refresh(workflow_run)
|
||||||
return convert_to_workflow_run(workflow_run)
|
return convert_to_workflow_run(workflow_run)
|
||||||
|
|
|
@ -211,6 +211,7 @@ class WorkflowRunModel(Base):
|
||||||
workflow_permanent_id = Column(String, nullable=False, index=True)
|
workflow_permanent_id = Column(String, nullable=False, index=True)
|
||||||
organization_id = Column(String, ForeignKey("organizations.organization_id"), nullable=False, index=True)
|
organization_id = Column(String, ForeignKey("organizations.organization_id"), nullable=False, index=True)
|
||||||
status = Column(String, nullable=False)
|
status = Column(String, nullable=False)
|
||||||
|
failure_reason = Column(String)
|
||||||
proxy_location = Column(Enum(ProxyLocation))
|
proxy_location = Column(Enum(ProxyLocation))
|
||||||
webhook_callback_url = Column(String)
|
webhook_callback_url = Column(String)
|
||||||
totp_verification_url = Column(String)
|
totp_verification_url = Column(String)
|
||||||
|
|
|
@ -191,6 +191,7 @@ def convert_to_workflow_run(workflow_run_model: WorkflowRunModel, debug_enabled:
|
||||||
workflow_id=workflow_run_model.workflow_id,
|
workflow_id=workflow_run_model.workflow_id,
|
||||||
organization_id=workflow_run_model.organization_id,
|
organization_id=workflow_run_model.organization_id,
|
||||||
status=WorkflowRunStatus[workflow_run_model.status],
|
status=WorkflowRunStatus[workflow_run_model.status],
|
||||||
|
failure_reason=workflow_run_model.failure_reason,
|
||||||
proxy_location=(
|
proxy_location=(
|
||||||
ProxyLocation(workflow_run_model.proxy_location) if workflow_run_model.proxy_location else None
|
ProxyLocation(workflow_run_model.proxy_location) if workflow_run_model.proxy_location else None
|
||||||
),
|
),
|
||||||
|
|
|
@ -26,6 +26,7 @@ from skyvern.exceptions import (
|
||||||
FailedToNavigateToUrl,
|
FailedToNavigateToUrl,
|
||||||
MissingBrowserState,
|
MissingBrowserState,
|
||||||
MissingBrowserStatePage,
|
MissingBrowserStatePage,
|
||||||
|
SkyvernException,
|
||||||
TaskNotFound,
|
TaskNotFound,
|
||||||
UnexpectedTaskStatus,
|
UnexpectedTaskStatus,
|
||||||
)
|
)
|
||||||
|
@ -39,7 +40,7 @@ from skyvern.forge.sdk.api.files import (
|
||||||
get_path_for_workflow_download_directory,
|
get_path_for_workflow_download_directory,
|
||||||
)
|
)
|
||||||
from skyvern.forge.sdk.api.llm.api_handler_factory import LLMAPIHandlerFactory
|
from skyvern.forge.sdk.api.llm.api_handler_factory import LLMAPIHandlerFactory
|
||||||
from skyvern.forge.sdk.schemas.tasks import TaskOutput, TaskStatus
|
from skyvern.forge.sdk.schemas.tasks import Task, TaskOutput, TaskStatus
|
||||||
from skyvern.forge.sdk.settings_manager import SettingsManager
|
from skyvern.forge.sdk.settings_manager import SettingsManager
|
||||||
from skyvern.forge.sdk.workflow.context_manager import WorkflowRunContext
|
from skyvern.forge.sdk.workflow.context_manager import WorkflowRunContext
|
||||||
from skyvern.forge.sdk.workflow.exceptions import (
|
from skyvern.forge.sdk.workflow.exceptions import (
|
||||||
|
@ -83,6 +84,7 @@ class BlockResult:
|
||||||
output_parameter: OutputParameter
|
output_parameter: OutputParameter
|
||||||
output_parameter_value: dict[str, Any] | list | str | None = None
|
output_parameter_value: dict[str, Any] | list | str | None = None
|
||||||
status: BlockStatus | None = None
|
status: BlockStatus | None = None
|
||||||
|
failure_reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class Block(BaseModel, abc.ABC):
|
class Block(BaseModel, abc.ABC):
|
||||||
|
@ -116,11 +118,13 @@ class Block(BaseModel, abc.ABC):
|
||||||
def build_block_result(
|
def build_block_result(
|
||||||
self,
|
self,
|
||||||
success: bool,
|
success: bool,
|
||||||
|
failure_reason: str | None,
|
||||||
output_parameter_value: dict[str, Any] | list | str | None = None,
|
output_parameter_value: dict[str, Any] | list | str | None = None,
|
||||||
status: BlockStatus | None = None,
|
status: BlockStatus | None = None,
|
||||||
) -> BlockResult:
|
) -> BlockResult:
|
||||||
return BlockResult(
|
return BlockResult(
|
||||||
success=success,
|
success=success,
|
||||||
|
failure_reason=failure_reason,
|
||||||
output_parameter=self.output_parameter,
|
output_parameter=self.output_parameter,
|
||||||
output_parameter_value=output_parameter_value,
|
output_parameter_value=output_parameter_value,
|
||||||
status=status,
|
status=status,
|
||||||
|
@ -145,7 +149,7 @@ class Block(BaseModel, abc.ABC):
|
||||||
async def execute_safe(self, workflow_run_id: str, **kwargs: dict) -> BlockResult:
|
async def execute_safe(self, workflow_run_id: str, **kwargs: dict) -> BlockResult:
|
||||||
try:
|
try:
|
||||||
return await self.execute(workflow_run_id, **kwargs)
|
return await self.execute(workflow_run_id, **kwargs)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
LOG.exception(
|
LOG.exception(
|
||||||
"Block execution failed",
|
"Block execution failed",
|
||||||
workflow_run_id=workflow_run_id,
|
workflow_run_id=workflow_run_id,
|
||||||
|
@ -156,7 +160,11 @@ class Block(BaseModel, abc.ABC):
|
||||||
workflow_run_context = self.get_workflow_run_context(workflow_run_id)
|
workflow_run_context = self.get_workflow_run_context(workflow_run_id)
|
||||||
if not workflow_run_context.has_value(self.output_parameter.key):
|
if not workflow_run_context.has_value(self.output_parameter.key):
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id)
|
||||||
return self.build_block_result(success=False, status=BlockStatus.failed)
|
|
||||||
|
failure_reason = "unexpected exception"
|
||||||
|
if isinstance(e, SkyvernException):
|
||||||
|
failure_reason = f"unexpected SkyvernException({e.__class__.__name__})"
|
||||||
|
return self.build_block_result(success=False, failure_reason=failure_reason, status=BlockStatus.failed)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_all_parameters(
|
def get_all_parameters(
|
||||||
|
@ -233,6 +241,7 @@ class TaskBlock(Block):
|
||||||
current_retry = 0
|
current_retry = 0
|
||||||
# initial value for will_retry is True, so that the loop runs at least once
|
# initial value for will_retry is True, so that the loop runs at least once
|
||||||
will_retry = True
|
will_retry = True
|
||||||
|
current_running_task: Task | None = None
|
||||||
workflow_run = await app.WORKFLOW_SERVICE.get_workflow_run(workflow_run_id=workflow_run_id)
|
workflow_run = await app.WORKFLOW_SERVICE.get_workflow_run(workflow_run_id=workflow_run_id)
|
||||||
workflow = await app.WORKFLOW_SERVICE.get_workflow(workflow_id=workflow_run.workflow_id)
|
workflow = await app.WORKFLOW_SERVICE.get_workflow(workflow_id=workflow_run.workflow_id)
|
||||||
# if the task url is parameterized, we need to get the value from the workflow run context
|
# if the task url is parameterized, we need to get the value from the workflow run context
|
||||||
|
@ -283,6 +292,7 @@ class TaskBlock(Block):
|
||||||
task_order=task_order,
|
task_order=task_order,
|
||||||
task_retry=task_retry,
|
task_retry=task_retry,
|
||||||
)
|
)
|
||||||
|
current_running_task = task
|
||||||
organization = await app.DATABASE.get_organization(organization_id=workflow.organization_id)
|
organization = await app.DATABASE.get_organization(organization_id=workflow.organization_id)
|
||||||
if not organization:
|
if not organization:
|
||||||
raise Exception(f"Organization is missing organization_id={workflow.organization_id}")
|
raise Exception(f"Organization is missing organization_id={workflow.organization_id}")
|
||||||
|
@ -353,6 +363,7 @@ class TaskBlock(Block):
|
||||||
raise TaskNotFound(task.task_id)
|
raise TaskNotFound(task.task_id)
|
||||||
if not updated_task.status.is_final():
|
if not updated_task.status.is_final():
|
||||||
raise UnexpectedTaskStatus(task_id=updated_task.task_id, status=updated_task.status)
|
raise UnexpectedTaskStatus(task_id=updated_task.task_id, status=updated_task.status)
|
||||||
|
current_running_task = updated_task
|
||||||
|
|
||||||
block_status_mapping = {
|
block_status_mapping = {
|
||||||
TaskStatus.completed: BlockStatus.completed,
|
TaskStatus.completed: BlockStatus.completed,
|
||||||
|
@ -375,6 +386,7 @@ class TaskBlock(Block):
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, output_parameter_value)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, output_parameter_value)
|
||||||
return self.build_block_result(
|
return self.build_block_result(
|
||||||
success=success,
|
success=success,
|
||||||
|
failure_reason=updated_task.failure_reason,
|
||||||
output_parameter_value=output_parameter_value,
|
output_parameter_value=output_parameter_value,
|
||||||
status=block_status_mapping[updated_task.status],
|
status=block_status_mapping[updated_task.status],
|
||||||
)
|
)
|
||||||
|
@ -388,7 +400,10 @@ class TaskBlock(Block):
|
||||||
organization_id=workflow.organization_id,
|
organization_id=workflow.organization_id,
|
||||||
)
|
)
|
||||||
return self.build_block_result(
|
return self.build_block_result(
|
||||||
success=False, output_parameter_value=None, status=block_status_mapping[updated_task.status]
|
success=False,
|
||||||
|
failure_reason=updated_task.failure_reason,
|
||||||
|
output_parameter_value=None,
|
||||||
|
status=block_status_mapping[updated_task.status],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
current_retry += 1
|
current_retry += 1
|
||||||
|
@ -413,12 +428,17 @@ class TaskBlock(Block):
|
||||||
)
|
)
|
||||||
return self.build_block_result(
|
return self.build_block_result(
|
||||||
success=False,
|
success=False,
|
||||||
|
failure_reason=updated_task.failure_reason,
|
||||||
output_parameter_value=output_parameter_value,
|
output_parameter_value=output_parameter_value,
|
||||||
status=block_status_mapping[updated_task.status],
|
status=block_status_mapping[updated_task.status],
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id)
|
||||||
return self.build_block_result(success=False, status=BlockStatus.failed)
|
return self.build_block_result(
|
||||||
|
success=False,
|
||||||
|
status=BlockStatus.failed,
|
||||||
|
failure_reason=current_running_task.failure_reason if current_running_task else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ForLoopBlock(Block):
|
class ForLoopBlock(Block):
|
||||||
|
@ -520,19 +540,37 @@ class ForLoopBlock(Block):
|
||||||
num_loop_over_values=len(loop_over_values),
|
num_loop_over_values=len(loop_over_values),
|
||||||
)
|
)
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, [])
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, [])
|
||||||
return self.build_block_result(success=False, status=BlockStatus.terminated)
|
return self.build_block_result(
|
||||||
|
success=False,
|
||||||
|
failure_reason="No iterable value found for the loop block",
|
||||||
|
status=BlockStatus.terminated,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.loop_blocks or len(self.loop_blocks) == 0:
|
||||||
|
LOG.info(
|
||||||
|
"No defined blocks to loop, terminating block",
|
||||||
|
block_type=self.block_type,
|
||||||
|
workflow_run_id=workflow_run_id,
|
||||||
|
num_loop_blocks=len(self.loop_blocks),
|
||||||
|
)
|
||||||
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, [])
|
||||||
|
return self.build_block_result(
|
||||||
|
success=False, failure_reason="No defined blocks to loop", status=BlockStatus.terminated
|
||||||
|
)
|
||||||
|
|
||||||
|
block_outputs: list[BlockResult] = []
|
||||||
for loop_idx, loop_over_value in enumerate(loop_over_values):
|
for loop_idx, loop_over_value in enumerate(loop_over_values):
|
||||||
context_parameters_with_value = self.get_loop_block_context_parameters(workflow_run_id, loop_over_value)
|
context_parameters_with_value = self.get_loop_block_context_parameters(workflow_run_id, loop_over_value)
|
||||||
for context_parameter in context_parameters_with_value:
|
for context_parameter in context_parameters_with_value:
|
||||||
workflow_run_context.set_value(context_parameter.key, context_parameter.value)
|
workflow_run_context.set_value(context_parameter.key, context_parameter.value)
|
||||||
block_outputs = []
|
|
||||||
for block_idx, loop_block in enumerate(self.loop_blocks):
|
for block_idx, loop_block in enumerate(self.loop_blocks):
|
||||||
original_loop_block = loop_block
|
original_loop_block = loop_block
|
||||||
loop_block = loop_block.copy()
|
loop_block = loop_block.copy()
|
||||||
block_output = await loop_block.execute_safe(workflow_run_id=workflow_run_id)
|
block_output = await loop_block.execute_safe(workflow_run_id=workflow_run_id)
|
||||||
if block_output.status == BlockStatus.canceled:
|
if block_output.status == BlockStatus.canceled:
|
||||||
|
failure_message = f"ForLoopBlock: Block with type {loop_block.block_type} at index {block_idx} during loop {loop_idx} was canceled for workflow run {workflow_run_id}, canceling for loop"
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"ForLoopBlock: Block with type {loop_block.block_type} at index {block_idx} was canceled for workflow run {workflow_run_id}, canceling for loop",
|
failure_message,
|
||||||
block_type=loop_block.block_type,
|
block_type=loop_block.block_type,
|
||||||
workflow_run_id=workflow_run_id,
|
workflow_run_id=workflow_run_id,
|
||||||
block_idx=block_idx,
|
block_idx=block_idx,
|
||||||
|
@ -542,7 +580,10 @@ class ForLoopBlock(Block):
|
||||||
workflow_run_context, workflow_run_id, outputs_with_loop_values
|
workflow_run_context, workflow_run_id, outputs_with_loop_values
|
||||||
)
|
)
|
||||||
return self.build_block_result(
|
return self.build_block_result(
|
||||||
success=False, output_parameter_value=outputs_with_loop_values, status=BlockStatus.canceled
|
success=False,
|
||||||
|
failure_reason=failure_message,
|
||||||
|
output_parameter_value=outputs_with_loop_values,
|
||||||
|
status=BlockStatus.canceled,
|
||||||
)
|
)
|
||||||
|
|
||||||
loop_block = original_loop_block
|
loop_block = original_loop_block
|
||||||
|
@ -580,6 +621,9 @@ class ForLoopBlock(Block):
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# at least one block must be executed in the loop
|
||||||
|
assert len(block_outputs) != 0
|
||||||
|
|
||||||
is_any_block_terminated = any([block_output.status == BlockStatus.terminated for block_output in block_outputs])
|
is_any_block_terminated = any([block_output.status == BlockStatus.terminated for block_output in block_outputs])
|
||||||
for_loop_block_status = BlockStatus.completed
|
for_loop_block_status = BlockStatus.completed
|
||||||
if is_any_block_terminated:
|
if is_any_block_terminated:
|
||||||
|
@ -588,7 +632,10 @@ class ForLoopBlock(Block):
|
||||||
for_loop_block_status = BlockStatus.failed
|
for_loop_block_status = BlockStatus.failed
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, outputs_with_loop_values)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, outputs_with_loop_values)
|
||||||
return self.build_block_result(
|
return self.build_block_result(
|
||||||
success=success, output_parameter_value=outputs_with_loop_values, status=for_loop_block_status
|
success=success,
|
||||||
|
failure_reason=block_outputs[-1].failure_reason,
|
||||||
|
output_parameter_value=outputs_with_loop_values,
|
||||||
|
status=for_loop_block_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -710,7 +757,9 @@ class TextPromptBlock(Block):
|
||||||
|
|
||||||
response = await self.send_prompt(self.prompt, parameter_values)
|
response = await self.send_prompt(self.prompt, parameter_values)
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, response)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, response)
|
||||||
return self.build_block_result(success=True, output_parameter_value=response, status=BlockStatus.completed)
|
return self.build_block_result(
|
||||||
|
success=True, failure_reason=None, output_parameter_value=response, status=BlockStatus.completed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DownloadToS3Block(Block):
|
class DownloadToS3Block(Block):
|
||||||
|
@ -767,7 +816,9 @@ class DownloadToS3Block(Block):
|
||||||
|
|
||||||
LOG.info("DownloadToS3Block: File downloaded and uploaded to S3", uri=uri)
|
LOG.info("DownloadToS3Block: File downloaded and uploaded to S3", uri=uri)
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, uri)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, uri)
|
||||||
return self.build_block_result(success=True, output_parameter_value=uri, status=BlockStatus.completed)
|
return self.build_block_result(
|
||||||
|
success=True, failure_reason=None, output_parameter_value=uri, status=BlockStatus.completed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadToS3Block(Block):
|
class UploadToS3Block(Block):
|
||||||
|
@ -841,7 +892,9 @@ class UploadToS3Block(Block):
|
||||||
|
|
||||||
LOG.info("UploadToS3Block: File(s) uploaded to S3", file_path=self.path)
|
LOG.info("UploadToS3Block: File(s) uploaded to S3", file_path=self.path)
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, s3_uris)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, s3_uris)
|
||||||
return self.build_block_result(success=True, output_parameter_value=s3_uris, status=BlockStatus.completed)
|
return self.build_block_result(
|
||||||
|
success=True, failure_reason=None, output_parameter_value=s3_uris, status=BlockStatus.completed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SendEmailBlock(Block):
|
class SendEmailBlock(Block):
|
||||||
|
@ -1109,14 +1162,18 @@ class SendEmailBlock(Block):
|
||||||
LOG.error("SendEmailBlock: Failed to send email", exc_info=True)
|
LOG.error("SendEmailBlock: Failed to send email", exc_info=True)
|
||||||
result_dict = {"success": False, "error": str(e)}
|
result_dict = {"success": False, "error": str(e)}
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, result_dict)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, result_dict)
|
||||||
return self.build_block_result(success=False, output_parameter_value=result_dict, status=BlockStatus.failed)
|
return self.build_block_result(
|
||||||
|
success=False, failure_reason=str(e), output_parameter_value=result_dict, status=BlockStatus.failed
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
if smtp_host:
|
if smtp_host:
|
||||||
smtp_host.quit()
|
smtp_host.quit()
|
||||||
|
|
||||||
result_dict = {"success": True}
|
result_dict = {"success": True}
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, result_dict)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, result_dict)
|
||||||
return self.build_block_result(success=True, output_parameter_value=result_dict, status=BlockStatus.completed)
|
return self.build_block_result(
|
||||||
|
success=True, failure_reason=None, output_parameter_value=result_dict, status=BlockStatus.completed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FileType(StrEnum):
|
class FileType(StrEnum):
|
||||||
|
@ -1179,7 +1236,9 @@ class FileParserBlock(Block):
|
||||||
parsed_data.append(row)
|
parsed_data.append(row)
|
||||||
# Record the parsed data
|
# Record the parsed data
|
||||||
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, parsed_data)
|
await self.record_output_parameter_value(workflow_run_context, workflow_run_id, parsed_data)
|
||||||
return self.build_block_result(success=True, output_parameter_value=parsed_data, status=BlockStatus.completed)
|
return self.build_block_result(
|
||||||
|
success=True, failure_reason=None, output_parameter_value=parsed_data, status=BlockStatus.completed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
BlockSubclasses = Union[
|
BlockSubclasses = Union[
|
||||||
|
|
|
@ -90,6 +90,7 @@ class WorkflowRun(BaseModel):
|
||||||
webhook_callback_url: str | None = None
|
webhook_callback_url: str | None = None
|
||||||
totp_verification_url: str | None = None
|
totp_verification_url: str | None = None
|
||||||
totp_identifier: str | None = None
|
totp_identifier: str | None = None
|
||||||
|
failure_reason: str | None = None
|
||||||
|
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
modified_at: datetime
|
modified_at: datetime
|
||||||
|
|
|
@ -6,7 +6,13 @@ import httpx
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from skyvern import analytics
|
from skyvern import analytics
|
||||||
from skyvern.exceptions import FailedToSendWebhook, MissingValueForParameter, WorkflowNotFound, WorkflowRunNotFound
|
from skyvern.exceptions import (
|
||||||
|
FailedToSendWebhook,
|
||||||
|
MissingValueForParameter,
|
||||||
|
SkyvernException,
|
||||||
|
WorkflowNotFound,
|
||||||
|
WorkflowRunNotFound,
|
||||||
|
)
|
||||||
from skyvern.forge import app
|
from skyvern.forge import app
|
||||||
from skyvern.forge.sdk.artifact.models import ArtifactType
|
from skyvern.forge.sdk.artifact.models import ArtifactType
|
||||||
from skyvern.forge.sdk.core import skyvern_context
|
from skyvern.forge.sdk.core import skyvern_context
|
||||||
|
@ -147,7 +153,14 @@ class WorkflowService:
|
||||||
f"Error while setting up workflow run {workflow_run.workflow_run_id}",
|
f"Error while setting up workflow run {workflow_run.workflow_run_id}",
|
||||||
workflow_run_id=workflow_run.workflow_run_id,
|
workflow_run_id=workflow_run.workflow_run_id,
|
||||||
)
|
)
|
||||||
await self.mark_workflow_run_as_failed(workflow_run_id=workflow_run.workflow_run_id)
|
|
||||||
|
failure_reason = "Setup up workflow failed due to an unexpected exception"
|
||||||
|
if isinstance(e, SkyvernException):
|
||||||
|
failure_reason = f"Setup workflow failed due to an SkyvernException({e.__class__.__name__})"
|
||||||
|
|
||||||
|
await self.mark_workflow_run_as_failed(
|
||||||
|
workflow_run_id=workflow_run.workflow_run_id, failure_reason=failure_reason
|
||||||
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return workflow_run
|
return workflow_run
|
||||||
|
@ -260,7 +273,10 @@ class WorkflowService:
|
||||||
block_label=block.label,
|
block_label=block.label,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.mark_workflow_run_as_failed(workflow_run_id=workflow_run.workflow_run_id)
|
failure_reason = f"Block with type {block.block_type} at index {block_idx}/{blocks_cnt -1} failed. failure reason: {block_result.failure_reason}"
|
||||||
|
await self.mark_workflow_run_as_failed(
|
||||||
|
workflow_run_id=workflow_run.workflow_run_id, failure_reason=failure_reason
|
||||||
|
)
|
||||||
await self.clean_up_workflow(
|
await self.clean_up_workflow(
|
||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
workflow_run=workflow_run,
|
workflow_run=workflow_run,
|
||||||
|
@ -289,14 +305,17 @@ class WorkflowService:
|
||||||
block_label=block.label,
|
block_label=block.label,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.mark_workflow_run_as_terminated(workflow_run_id=workflow_run.workflow_run_id)
|
failure_reason = f"Block with type {block.block_type} at index {block_idx}/{blocks_cnt -1} terminated. Reason: {block_result.failure_reason}"
|
||||||
|
await self.mark_workflow_run_as_terminated(
|
||||||
|
workflow_run_id=workflow_run.workflow_run_id, failure_reason=failure_reason
|
||||||
|
)
|
||||||
await self.clean_up_workflow(
|
await self.clean_up_workflow(
|
||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
workflow_run=workflow_run,
|
workflow_run=workflow_run,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
return workflow_run
|
return workflow_run
|
||||||
except Exception:
|
except Exception as e:
|
||||||
LOG.exception(
|
LOG.exception(
|
||||||
f"Error while executing workflow run {workflow_run.workflow_run_id}",
|
f"Error while executing workflow run {workflow_run.workflow_run_id}",
|
||||||
workflow_run_id=workflow_run.workflow_run_id,
|
workflow_run_id=workflow_run.workflow_run_id,
|
||||||
|
@ -304,7 +323,15 @@ class WorkflowService:
|
||||||
block_type=block.block_type,
|
block_type=block.block_type,
|
||||||
block_label=block.label,
|
block_label=block.label,
|
||||||
)
|
)
|
||||||
await self.mark_workflow_run_as_failed(workflow_run_id=workflow_run.workflow_run_id)
|
|
||||||
|
exception_message = "unexpected exception"
|
||||||
|
if isinstance(e, SkyvernException):
|
||||||
|
exception_message = f"unexpected SkyvernException({e.__class__.__name__})"
|
||||||
|
|
||||||
|
failure_reason = f"Block with type {block.block_type} at index {block_idx}/{blocks_cnt -1} failed. failure reason: {exception_message}"
|
||||||
|
await self.mark_workflow_run_as_failed(
|
||||||
|
workflow_run_id=workflow_run.workflow_run_id, failure_reason=failure_reason
|
||||||
|
)
|
||||||
await self.clean_up_workflow(workflow=workflow, workflow_run=workflow_run, api_key=api_key)
|
await self.clean_up_workflow(workflow=workflow, workflow_run=workflow_run, api_key=api_key)
|
||||||
return workflow_run
|
return workflow_run
|
||||||
|
|
||||||
|
@ -472,15 +499,17 @@ class WorkflowService:
|
||||||
status=WorkflowRunStatus.completed,
|
status=WorkflowRunStatus.completed,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mark_workflow_run_as_failed(self, workflow_run_id: str) -> None:
|
async def mark_workflow_run_as_failed(self, workflow_run_id: str, failure_reason: str | None) -> None:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"Marking workflow run {workflow_run_id} as failed",
|
f"Marking workflow run {workflow_run_id} as failed",
|
||||||
workflow_run_id=workflow_run_id,
|
workflow_run_id=workflow_run_id,
|
||||||
workflow_status="failed",
|
workflow_status="failed",
|
||||||
|
failure_reason=failure_reason,
|
||||||
)
|
)
|
||||||
await app.DATABASE.update_workflow_run(
|
await app.DATABASE.update_workflow_run(
|
||||||
workflow_run_id=workflow_run_id,
|
workflow_run_id=workflow_run_id,
|
||||||
status=WorkflowRunStatus.failed,
|
status=WorkflowRunStatus.failed,
|
||||||
|
failure_reason=failure_reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mark_workflow_run_as_running(self, workflow_run_id: str) -> None:
|
async def mark_workflow_run_as_running(self, workflow_run_id: str) -> None:
|
||||||
|
@ -494,15 +523,17 @@ class WorkflowService:
|
||||||
status=WorkflowRunStatus.running,
|
status=WorkflowRunStatus.running,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mark_workflow_run_as_terminated(self, workflow_run_id: str) -> None:
|
async def mark_workflow_run_as_terminated(self, workflow_run_id: str, failure_reason: str | None) -> None:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"Marking workflow run {workflow_run_id} as terminated",
|
f"Marking workflow run {workflow_run_id} as terminated",
|
||||||
workflow_run_id=workflow_run_id,
|
workflow_run_id=workflow_run_id,
|
||||||
workflow_status="terminated",
|
workflow_status="terminated",
|
||||||
|
failure_reason=failure_reason,
|
||||||
)
|
)
|
||||||
await app.DATABASE.update_workflow_run(
|
await app.DATABASE.update_workflow_run(
|
||||||
workflow_run_id=workflow_run_id,
|
workflow_run_id=workflow_run_id,
|
||||||
status=WorkflowRunStatus.terminated,
|
status=WorkflowRunStatus.terminated,
|
||||||
|
failure_reason=failure_reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def mark_workflow_run_as_canceled(self, workflow_run_id: str) -> None:
|
async def mark_workflow_run_as_canceled(self, workflow_run_id: str) -> None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue