Workflow Copilot: review and approve/reject changes (#4559)

This commit is contained in:
Stanislav Novosad
2026-01-27 13:24:44 -07:00
committed by GitHub
parent cb2a72775d
commit c0f361bb6e
10 changed files with 481 additions and 80 deletions

View File

@@ -140,6 +140,7 @@ from skyvern.schemas.workflows import BlockStatus, BlockType, WorkflowStatus
from skyvern.webeye.actions.actions import Action
LOG = structlog.get_logger()
_UNSET = object()
def _serialize_proxy_location(proxy_location: ProxyLocationInput) -> str | None:
@@ -3667,6 +3668,33 @@ class AgentDB(BaseAlchemyDB):
await session.refresh(new_chat)
return WorkflowCopilotChat.model_validate(new_chat)
async def update_workflow_copilot_chat(
self,
organization_id: str,
workflow_copilot_chat_id: str,
proposed_workflow: dict | None | object = _UNSET,
auto_accept: bool | None = None,
) -> WorkflowCopilotChat | None:
async with self.Session() as session:
chat = (
await session.scalars(
select(WorkflowCopilotChatModel)
.where(WorkflowCopilotChatModel.organization_id == organization_id)
.where(WorkflowCopilotChatModel.workflow_copilot_chat_id == workflow_copilot_chat_id)
)
).first()
if not chat:
return None
if proposed_workflow is not _UNSET:
chat.proposed_workflow = proposed_workflow
if auto_accept is not None:
chat.auto_accept = auto_accept
await session.commit()
await session.refresh(chat)
return WorkflowCopilotChat.model_validate(chat)
async def create_workflow_copilot_chat_message(
self,
organization_id: str,

View File

@@ -1098,6 +1098,8 @@ class WorkflowCopilotChatModel(Base):
workflow_copilot_chat_id = Column(String, primary_key=True, default=generate_workflow_copilot_chat_id)
organization_id = Column(String, nullable=False)
workflow_permanent_id = Column(String, nullable=False, index=True)
proposed_workflow = Column(JSON, nullable=True)
auto_accept = Column(Boolean, nullable=True, default=False)
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column(

View File

@@ -26,6 +26,7 @@ from skyvern.forge.sdk.schemas.workflow_copilot import (
WorkflowCopilotChatMessage,
WorkflowCopilotChatRequest,
WorkflowCopilotChatSender,
WorkflowCopilotClearProposedWorkflowRequest,
WorkflowCopilotProcessingUpdate,
WorkflowCopilotStreamErrorUpdate,
WorkflowCopilotStreamMessageType,
@@ -448,6 +449,13 @@ async def workflow_copilot_chat_post(
)
return
if updated_workflow and chat.auto_accept is not True:
await app.DATABASE.update_workflow_copilot_chat(
organization_id=chat.organization_id,
workflow_copilot_chat_id=chat.workflow_copilot_chat_id,
proposed_workflow=updated_workflow.model_dump(mode="json"),
)
await app.DATABASE.create_workflow_copilot_chat_message(
organization_id=chat.organization_id,
workflow_copilot_chat_id=chat.workflow_copilot_chat_id,
@@ -518,17 +526,35 @@ async def workflow_copilot_chat_history(
organization_id=organization.organization_id,
workflow_permanent_id=workflow_permanent_id,
)
if not latest_chat:
return WorkflowCopilotChatHistoryResponse(workflow_copilot_chat_id=None, chat_history=[])
chat_messages = await app.DATABASE.get_workflow_copilot_chat_messages(
workflow_copilot_chat_id=latest_chat.workflow_copilot_chat_id,
)
if latest_chat:
chat_messages = await app.DATABASE.get_workflow_copilot_chat_messages(latest_chat.workflow_copilot_chat_id)
else:
chat_messages = []
return WorkflowCopilotChatHistoryResponse(
workflow_copilot_chat_id=latest_chat.workflow_copilot_chat_id,
workflow_copilot_chat_id=latest_chat.workflow_copilot_chat_id if latest_chat else None,
chat_history=convert_to_history_messages(chat_messages),
proposed_workflow=latest_chat.proposed_workflow if latest_chat else None,
auto_accept=latest_chat.auto_accept if latest_chat else None,
)
@base_router.post(
"/workflow/copilot/clear-proposed-workflow", include_in_schema=False, status_code=status.HTTP_204_NO_CONTENT
)
async def workflow_copilot_clear_proposed_workflow(
clear_request: WorkflowCopilotClearProposedWorkflowRequest,
organization: Organization = Depends(org_auth_service.get_current_org),
) -> None:
updated_chat = await app.DATABASE.update_workflow_copilot_chat(
organization_id=organization.organization_id,
workflow_copilot_chat_id=clear_request.workflow_copilot_chat_id,
proposed_workflow=None,
auto_accept=clear_request.auto_accept,
)
if not updated_chat:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found")
def convert_to_history_messages(
messages: list[WorkflowCopilotChatMessage],
) -> list[WorkflowCopilotChatHistoryMessage]:

View File

@@ -10,6 +10,8 @@ class WorkflowCopilotChat(BaseModel):
workflow_copilot_chat_id: str = Field(..., description="ID for the workflow copilot chat")
organization_id: str = Field(..., description="Organization ID for the chat")
workflow_permanent_id: str = Field(..., description="Workflow permanent ID for the chat")
proposed_workflow: dict | None = Field(None, description="Latest workflow proposed by the copilot")
auto_accept: bool | None = Field(False, description="Whether copilot auto-accepts workflow updates")
created_at: datetime = Field(..., description="When the chat was created")
modified_at: datetime = Field(..., description="When the chat was last modified")
@@ -40,6 +42,11 @@ class WorkflowCopilotChatRequest(BaseModel):
workflow_yaml: str = Field(..., description="Current workflow YAML including unsaved changes")
class WorkflowCopilotClearProposedWorkflowRequest(BaseModel):
workflow_copilot_chat_id: str = Field(..., description="The chat ID to update")
auto_accept: bool = Field(..., description="Whether to auto-accept future workflow updates")
class WorkflowCopilotChatHistoryMessage(BaseModel):
sender: WorkflowCopilotChatSender = Field(..., description="Message sender")
content: str = Field(..., description="Message content")
@@ -49,6 +56,8 @@ class WorkflowCopilotChatHistoryMessage(BaseModel):
class WorkflowCopilotChatHistoryResponse(BaseModel):
workflow_copilot_chat_id: str | None = Field(None, description="Latest chat ID for the workflow")
chat_history: list[WorkflowCopilotChatHistoryMessage] = Field(default_factory=list, description="Chat messages")
proposed_workflow: dict | None = Field(None, description="Latest workflow proposed by the copilot")
auto_accept: bool | None = Field(None, description="Whether copilot auto-accepts workflow updates")
class WorkflowCopilotStreamMessageType(StrEnum):