Copilot: fix review button by conversion YAML server side (#4573)
This commit is contained in:
committed by
GitHub
parent
9c2b4c690e
commit
6faca9c558
@@ -1,4 +1,7 @@
|
||||
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
WorkflowApiResponse,
|
||||
WorkflowDefinition,
|
||||
} from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type WorkflowCopilotChatSender = "user" | "ai";
|
||||
|
||||
@@ -70,3 +73,12 @@ export interface WorkflowCopilotStreamErrorUpdate {
|
||||
type: "error";
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface WorkflowYAMLConversionRequest {
|
||||
workflow_definition_yaml: string;
|
||||
workflow_id: string;
|
||||
}
|
||||
|
||||
export interface WorkflowYAMLConversionResponse {
|
||||
workflow_definition: WorkflowDefinition;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MutableRefObject,
|
||||
} from "react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { stringify as convertToYAML } from "yaml";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
@@ -94,11 +95,7 @@ import {
|
||||
import { WorkflowHeader } from "./WorkflowHeader";
|
||||
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
|
||||
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
|
||||
import {
|
||||
Parameter,
|
||||
WorkflowBlock,
|
||||
WorkflowSettings,
|
||||
} from "../types/workflowTypes";
|
||||
import { WorkflowSettings } from "../types/workflowTypes";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import {
|
||||
nodeAdderNode,
|
||||
@@ -112,6 +109,7 @@ import { constructCacheKeyValue, getInitialParameters } from "./utils";
|
||||
import { WorkflowCopilotChat } from "../copilot/WorkflowCopilotChat";
|
||||
import { WorkflowCopilotButton } from "../copilot/WorkflowCopilotButton";
|
||||
import type { CopilotReviewStatus } from "./panels/WorkflowComparisonPanel";
|
||||
import type { WorkflowYAMLConversionResponse } from "../copilot/workflowCopilotTypes";
|
||||
import "./workspace-styles.css";
|
||||
|
||||
const Constants = {
|
||||
@@ -1743,105 +1741,133 @@ function Workspace({
|
||||
onClose={() => setIsCopilotOpen(false)}
|
||||
onMessageCountChange={setCopilotMessageCount}
|
||||
buttonRef={copilotButtonRef}
|
||||
onReviewWorkflow={(pendingWorkflow, clearPending) => {
|
||||
onReviewWorkflow={async (pendingWorkflow, clearPending) => {
|
||||
const saveData = workflowChangesStore.getSaveData?.();
|
||||
if (!saveData) return;
|
||||
|
||||
// Construct fake WorkflowVersion for current state
|
||||
const currentVersion: WorkflowVersion = {
|
||||
workflow_id: saveData.workflow.workflow_id,
|
||||
organization_id: "",
|
||||
is_saved_task: saveData.workflow.is_saved_task ?? false,
|
||||
is_template: false,
|
||||
title: "Current",
|
||||
workflow_permanent_id: saveData.workflow.workflow_permanent_id,
|
||||
version: saveData.workflow.version ?? 0,
|
||||
description: saveData.workflow.description ?? "",
|
||||
workflow_definition: {
|
||||
parameters: saveData.parameters as unknown as Parameter[],
|
||||
blocks: saveData.blocks as unknown as WorkflowBlock[],
|
||||
finally_block_label: saveData.settings.finallyBlockLabel,
|
||||
},
|
||||
proxy_location: saveData.settings.proxyLocation,
|
||||
webhook_callback_url: saveData.settings.webhookCallbackUrl,
|
||||
extra_http_headers: saveData.settings.extraHttpHeaders
|
||||
? JSON.parse(saveData.settings.extraHttpHeaders)
|
||||
: null,
|
||||
persist_browser_session: saveData.settings.persistBrowserSession,
|
||||
model: saveData.settings.model,
|
||||
totp_verification_url: saveData.workflow.totp_verification_url,
|
||||
totp_identifier: null,
|
||||
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
|
||||
status: saveData.workflow.status,
|
||||
created_at: new Date().toISOString(),
|
||||
modified_at: new Date().toISOString(),
|
||||
deleted_at: null,
|
||||
run_with: saveData.settings.runWith,
|
||||
cache_key: saveData.settings.scriptCacheKey,
|
||||
ai_fallback: saveData.settings.aiFallback,
|
||||
run_sequentially: saveData.settings.runSequentially,
|
||||
sequential_key: saveData.settings.sequentialKey,
|
||||
folder_id: null,
|
||||
import_error: null,
|
||||
};
|
||||
try {
|
||||
// Create YAML from current workflow definition only
|
||||
const workflowDefinitionYaml = convertToYAML({
|
||||
version: saveData.workflowDefinitionVersion,
|
||||
parameters: saveData.parameters,
|
||||
blocks: saveData.blocks,
|
||||
finally_block_label:
|
||||
saveData.settings.finallyBlockLabel ?? undefined,
|
||||
});
|
||||
|
||||
// Construct fake WorkflowVersion for pending copilot suggestion
|
||||
const pendingVersion: WorkflowVersion = {
|
||||
...pendingWorkflow,
|
||||
title: "Copilot Suggestion",
|
||||
};
|
||||
// Convert current workflow definition YAML to blocks
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
||||
// Handle copilot review close with status
|
||||
const handleCopilotReviewClose = (status: CopilotReviewStatus) => {
|
||||
if (status === "approve") {
|
||||
try {
|
||||
applyWorkflowUpdate(pendingWorkflow);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to apply copilot workflow",
|
||||
error,
|
||||
pendingWorkflow,
|
||||
);
|
||||
toast({
|
||||
title: "Update failed",
|
||||
description:
|
||||
"Failed to apply workflow update. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
const currentConversionResponse =
|
||||
await client.post<WorkflowYAMLConversionResponse>(
|
||||
"/workflow/copilot/convert-yaml-to-blocks",
|
||||
{
|
||||
workflow_definition_yaml: workflowDefinitionYaml,
|
||||
workflow_id: saveData.workflow.workflow_id,
|
||||
},
|
||||
);
|
||||
|
||||
// Construct WorkflowVersion for current state with converted blocks
|
||||
const currentVersion: WorkflowVersion = {
|
||||
workflow_id: saveData.workflow.workflow_id,
|
||||
organization_id: "",
|
||||
is_saved_task: saveData.workflow.is_saved_task ?? false,
|
||||
is_template: false,
|
||||
title: "Current",
|
||||
workflow_permanent_id: saveData.workflow.workflow_permanent_id,
|
||||
version: saveData.workflow.version ?? 0,
|
||||
description: saveData.workflow.description ?? "",
|
||||
workflow_definition:
|
||||
currentConversionResponse.data.workflow_definition,
|
||||
proxy_location: saveData.settings.proxyLocation,
|
||||
webhook_callback_url: saveData.settings.webhookCallbackUrl,
|
||||
extra_http_headers: saveData.settings.extraHttpHeaders
|
||||
? JSON.parse(saveData.settings.extraHttpHeaders)
|
||||
: null,
|
||||
persist_browser_session: saveData.settings.persistBrowserSession,
|
||||
model: saveData.settings.model,
|
||||
totp_verification_url: saveData.workflow.totp_verification_url,
|
||||
totp_identifier: null,
|
||||
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
|
||||
status: saveData.workflow.status,
|
||||
created_at: new Date().toISOString(),
|
||||
modified_at: new Date().toISOString(),
|
||||
deleted_at: null,
|
||||
run_with: saveData.settings.runWith,
|
||||
cache_key: saveData.settings.scriptCacheKey,
|
||||
ai_fallback: saveData.settings.aiFallback,
|
||||
run_sequentially: saveData.settings.runSequentially,
|
||||
sequential_key: saveData.settings.sequentialKey,
|
||||
folder_id: null,
|
||||
import_error: null,
|
||||
};
|
||||
|
||||
// Construct fake WorkflowVersion for pending copilot suggestion
|
||||
const pendingVersion: WorkflowVersion = {
|
||||
...pendingWorkflow,
|
||||
title: "Copilot Suggestion",
|
||||
};
|
||||
|
||||
// Handle copilot review close with status
|
||||
const handleCopilotReviewClose = (status: CopilotReviewStatus) => {
|
||||
if (status === "approve") {
|
||||
try {
|
||||
applyWorkflowUpdate(pendingWorkflow);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to apply copilot workflow",
|
||||
error,
|
||||
pendingWorkflow,
|
||||
);
|
||||
toast({
|
||||
title: "Update failed",
|
||||
description:
|
||||
"Failed to apply workflow update. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the panel and reopen copilot chat
|
||||
// Close the panel and reopen copilot chat
|
||||
setWorkflowPanelState({
|
||||
active: false,
|
||||
content: "history",
|
||||
data: {
|
||||
showComparison: false,
|
||||
version1: undefined,
|
||||
version2: undefined,
|
||||
},
|
||||
});
|
||||
setIsCopilotOpen(true);
|
||||
|
||||
// Clear pending for approve and reject, but not for close
|
||||
if (status !== "close") {
|
||||
clearPending();
|
||||
}
|
||||
};
|
||||
|
||||
// Hide chat and show comparison
|
||||
setIsCopilotOpen(false);
|
||||
setWorkflowPanelState({
|
||||
active: false,
|
||||
content: "history",
|
||||
data: {
|
||||
showComparison: false,
|
||||
version1: undefined,
|
||||
version2: undefined,
|
||||
version1: currentVersion,
|
||||
version2: pendingVersion,
|
||||
showComparison: true,
|
||||
mode: "copilot",
|
||||
onCopilotReviewClose: handleCopilotReviewClose,
|
||||
},
|
||||
});
|
||||
setIsCopilotOpen(true);
|
||||
|
||||
// Clear pending for approve and reject, but not for close
|
||||
if (status !== "close") {
|
||||
clearPending();
|
||||
}
|
||||
};
|
||||
|
||||
// Hide chat and show comparison
|
||||
setIsCopilotOpen(false);
|
||||
setWorkflowPanelState({
|
||||
active: false,
|
||||
content: "history",
|
||||
data: {
|
||||
version1: currentVersion,
|
||||
version2: pendingVersion,
|
||||
showComparison: true,
|
||||
mode: "copilot",
|
||||
onCopilotReviewClose: handleCopilotReviewClose,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to prepare workflow comparison", error);
|
||||
toast({
|
||||
title: "Comparison failed",
|
||||
description:
|
||||
"Failed to prepare workflow for comparison. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}}
|
||||
onWorkflowUpdate={(workflowData) => {
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,8 @@ from skyvern.forge.sdk.schemas.workflow_copilot import (
|
||||
WorkflowCopilotStreamErrorUpdate,
|
||||
WorkflowCopilotStreamMessageType,
|
||||
WorkflowCopilotStreamResponseUpdate,
|
||||
WorkflowYAMLConversionRequest,
|
||||
WorkflowYAMLConversionResponse,
|
||||
)
|
||||
from skyvern.forge.sdk.services import org_auth_service
|
||||
from skyvern.forge.sdk.workflow.exceptions import BaseWorkflowHTTPException
|
||||
@@ -40,6 +42,7 @@ from skyvern.forge.sdk.workflow.workflow_definition_converter import convert_wor
|
||||
from skyvern.schemas.workflows import (
|
||||
LoginBlockYAML,
|
||||
WorkflowCreateYAMLRequest,
|
||||
WorkflowDefinitionYAML,
|
||||
)
|
||||
|
||||
WORKFLOW_KNOWLEDGE_BASE_PATH = Path("skyvern/forge/prompts/skyvern/workflow_knowledge_base.txt")
|
||||
@@ -566,3 +569,30 @@ def convert_to_history_messages(
|
||||
)
|
||||
for message in messages
|
||||
]
|
||||
|
||||
|
||||
@base_router.post("/workflow/copilot/convert-yaml-to-blocks", include_in_schema=False)
|
||||
async def workflow_copilot_convert_yaml_to_blocks(
|
||||
request: WorkflowYAMLConversionRequest,
|
||||
organization: Organization = Depends(org_auth_service.get_current_org),
|
||||
) -> WorkflowYAMLConversionResponse:
|
||||
"""
|
||||
Convert workflow definition YAML to blocks format for comparison view.
|
||||
This endpoint is used by the frontend to convert YAML to the proper blocks structure
|
||||
that the comparison panel expects.
|
||||
"""
|
||||
try:
|
||||
parsed_yaml = yaml.safe_load(request.workflow_definition_yaml)
|
||||
workflow_definition_yaml = WorkflowDefinitionYAML.model_validate(parsed_yaml)
|
||||
|
||||
workflow_definition = convert_workflow_definition(
|
||||
workflow_definition_yaml=workflow_definition_yaml,
|
||||
workflow_id=request.workflow_id,
|
||||
)
|
||||
|
||||
return WorkflowYAMLConversionResponse(workflow_definition=workflow_definition.model_dump(mode="json"))
|
||||
except (yaml.YAMLError, ValidationError, BaseWorkflowHTTPException) as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Failed to convert workflow YAML: {str(e)}",
|
||||
)
|
||||
|
||||
@@ -87,3 +87,12 @@ class WorkflowCopilotStreamResponseUpdate(BaseModel):
|
||||
class WorkflowCopilotStreamErrorUpdate(BaseModel):
|
||||
type: WorkflowCopilotStreamMessageType = Field(WorkflowCopilotStreamMessageType.ERROR, description="Message type")
|
||||
error: str = Field(..., description="Error message")
|
||||
|
||||
|
||||
class WorkflowYAMLConversionRequest(BaseModel):
|
||||
workflow_definition_yaml: str = Field(..., description="Workflow definition YAML to convert to blocks")
|
||||
workflow_id: str = Field(..., description="Workflow ID")
|
||||
|
||||
|
||||
class WorkflowYAMLConversionResponse(BaseModel):
|
||||
workflow_definition: dict = Field(..., description="Converted workflow definition with blocks")
|
||||
|
||||
Reference in New Issue
Block a user