From 353358ee1764e69e0273b2c88b8eeb89e8de7a09 Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Mon, 27 Oct 2025 14:50:17 -0400 Subject: [PATCH] Jon/backend hitl buffs (#3826) --- ...add_fields_to_workflow_run_blocks_table.py | 35 ++++++++++++++ skyvern/forge/sdk/db/client.py | 11 +++++ skyvern/forge/sdk/db/models.py | 5 ++ skyvern/forge/sdk/db/utils.py | 3 ++ skyvern/forge/sdk/schemas/workflow_runs.py | 5 ++ skyvern/forge/sdk/workflow/models/block.py | 47 +++++++++++++------ 6 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 alembic/versions/2025_10_27_1843-d135e472622c_add_fields_to_workflow_run_blocks_table.py diff --git a/alembic/versions/2025_10_27_1843-d135e472622c_add_fields_to_workflow_run_blocks_table.py b/alembic/versions/2025_10_27_1843-d135e472622c_add_fields_to_workflow_run_blocks_table.py new file mode 100644 index 00000000..0d3055ac --- /dev/null +++ b/alembic/versions/2025_10_27_1843-d135e472622c_add_fields_to_workflow_run_blocks_table.py @@ -0,0 +1,35 @@ +"""add fields to workflow_run_blocks table + +Revision ID: d135e472622c +Revises: b80c42316c94 +Create Date: 2025-10-27 18:43:20.933288+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "d135e472622c" +down_revision: Union[str, None] = "b80c42316c94" +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_run_blocks", sa.Column("instructions", sa.String(), nullable=True)) + op.add_column("workflow_run_blocks", sa.Column("positive_descriptor", sa.String(), nullable=True)) + op.add_column("workflow_run_blocks", sa.Column("negative_descriptor", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("workflow_run_blocks", "negative_descriptor") + op.drop_column("workflow_run_blocks", "positive_descriptor") + op.drop_column("workflow_run_blocks", "instructions") + # ### end Alembic commands ### diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index db57254a..7158fe7c 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -3289,6 +3289,10 @@ class AgentDB: http_request_timeout: int | None = None, http_request_follow_redirects: bool | None = None, ai_fallback_triggered: bool | None = None, + # human interaction block + instructions: str | None = None, + positive_descriptor: str | None = None, + negative_descriptor: str | None = None, ) -> WorkflowRunBlock: async with self.Session() as session: workflow_run_block = ( @@ -3348,6 +3352,13 @@ class AgentDB: workflow_run_block.http_request_follow_redirects = http_request_follow_redirects if ai_fallback_triggered is not None: workflow_run_block.script_run = {"ai_fallback_triggered": ai_fallback_triggered} + # human interaction block fields + if instructions: + workflow_run_block.instructions = instructions + if positive_descriptor: + workflow_run_block.positive_descriptor = positive_descriptor + if negative_descriptor: + workflow_run_block.negative_descriptor = negative_descriptor await session.commit() await session.refresh(workflow_run_block) else: diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index 05d382f5..f67fc737 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -680,6 +680,11 @@ class WorkflowRunBlockModel(Base): http_request_timeout = Column(Integer, nullable=True) http_request_follow_redirects = Column(Boolean, nullable=True) + # human interaction block + instructions = Column(String, nullable=True) + positive_descriptor = Column(String, nullable=True) + negative_descriptor = Column(String, nullable=True) + created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False) diff --git a/skyvern/forge/sdk/db/utils.py b/skyvern/forge/sdk/db/utils.py index a1ef9d61..f5c8ca6d 100644 --- a/skyvern/forge/sdk/db/utils.py +++ b/skyvern/forge/sdk/db/utils.py @@ -523,6 +523,9 @@ def convert_to_workflow_run_block( body=workflow_run_block_model.body, created_at=workflow_run_block_model.created_at, modified_at=workflow_run_block_model.modified_at, + instructions=workflow_run_block_model.instructions, + positive_descriptor=workflow_run_block_model.positive_descriptor, + negative_descriptor=workflow_run_block_model.negative_descriptor, ) if task: if task.finished_at and task.started_at: diff --git a/skyvern/forge/sdk/schemas/workflow_runs.py b/skyvern/forge/sdk/schemas/workflow_runs.py index b8072947..c382084d 100644 --- a/skyvern/forge/sdk/schemas/workflow_runs.py +++ b/skyvern/forge/sdk/schemas/workflow_runs.py @@ -53,6 +53,11 @@ class WorkflowRunBlock(BaseModel): subject: str | None = None body: str | None = None + # human interaction block + instructions: str | None = None + positive_descriptor: str | None = None + negative_descriptor: str | None = None + class WorkflowRunTimelineType(StrEnum): thought = "thought" diff --git a/skyvern/forge/sdk/workflow/models/block.py b/skyvern/forge/sdk/workflow/models/block.py index c5b117a3..9f90f79a 100644 --- a/skyvern/forge/sdk/workflow/models/block.py +++ b/skyvern/forge/sdk/workflow/models/block.py @@ -3099,6 +3099,14 @@ class HumanInteractionBlock(BaseTaskBlock): self.recipients = formatted + self.negative_descriptor = self.format_block_parameter_template_from_workflow_run_context( + self.negative_descriptor, workflow_run_context + ) + + self.positive_descriptor = self.format_block_parameter_template_from_workflow_run_context( + self.positive_descriptor, workflow_run_context + ) + async def execute( self, workflow_run_id: str, @@ -3110,6 +3118,31 @@ class HumanInteractionBlock(BaseTaskBlock): # avoid circular import from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus # noqa: PLC0415 + workflow_run_context = self.get_workflow_run_context(workflow_run_id) + + try: + self.format_potential_template_parameters(workflow_run_context) + except Exception as e: + return await self.build_block_result( + success=False, + failure_reason=f"Failed to format jinja template: {str(e)}", + output_parameter_value=None, + status=BlockStatus.failed, + workflow_run_block_id=workflow_run_block_id, + organization_id=organization_id, + ) + + await app.DATABASE.update_workflow_run_block( + workflow_run_block_id=workflow_run_block_id, + organization_id=organization_id, + recipients=self.recipients, + subject=self.subject, + body=self.body, + instructions=self.instructions, + positive_descriptor=self.positive_descriptor, + negative_descriptor=self.negative_descriptor, + ) + LOG.info( "Pausing workflow for human interaction", workflow_run_id=workflow_run_id, @@ -3138,20 +3171,6 @@ class HumanInteractionBlock(BaseTaskBlock): organization_id=organization_id, ) - workflow_run_context = self.get_workflow_run_context(workflow_run_id) - - try: - self.format_potential_template_parameters(workflow_run_context) - except Exception as e: - return await self.build_block_result( - success=False, - failure_reason=f"Failed to format jinja template: {str(e)}", - output_parameter_value=None, - status=BlockStatus.failed, - workflow_run_block_id=workflow_run_block_id, - organization_id=organization_id, - ) - workflow_permanent_id = workflow_run.workflow_permanent_id app_url = f"{settings.SKYVERN_APP_URL}/workflows/{workflow_permanent_id}/{workflow_run_id}/overview" body = f"{self.body}\n\nKindly visit {app_url}\n\n{self.instructions}\n\n"