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";
|
export type WorkflowCopilotChatSender = "user" | "ai";
|
||||||
|
|
||||||
@@ -70,3 +73,12 @@ export interface WorkflowCopilotStreamErrorUpdate {
|
|||||||
type: "error";
|
type: "error";
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowYAMLConversionRequest {
|
||||||
|
workflow_definition_yaml: string;
|
||||||
|
workflow_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowYAMLConversionResponse {
|
||||||
|
workflow_definition: WorkflowDefinition;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { stringify as convertToYAML } from "yaml";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -94,11 +95,7 @@ import {
|
|||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
|
import { WorkflowHistoryPanel } from "./panels/WorkflowHistoryPanel";
|
||||||
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
|
import { WorkflowVersion } from "../hooks/useWorkflowVersionsQuery";
|
||||||
import {
|
import { WorkflowSettings } from "../types/workflowTypes";
|
||||||
Parameter,
|
|
||||||
WorkflowBlock,
|
|
||||||
WorkflowSettings,
|
|
||||||
} from "../types/workflowTypes";
|
|
||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation } from "@/api/types";
|
||||||
import {
|
import {
|
||||||
nodeAdderNode,
|
nodeAdderNode,
|
||||||
@@ -112,6 +109,7 @@ import { constructCacheKeyValue, getInitialParameters } from "./utils";
|
|||||||
import { WorkflowCopilotChat } from "../copilot/WorkflowCopilotChat";
|
import { WorkflowCopilotChat } from "../copilot/WorkflowCopilotChat";
|
||||||
import { WorkflowCopilotButton } from "../copilot/WorkflowCopilotButton";
|
import { WorkflowCopilotButton } from "../copilot/WorkflowCopilotButton";
|
||||||
import type { CopilotReviewStatus } from "./panels/WorkflowComparisonPanel";
|
import type { CopilotReviewStatus } from "./panels/WorkflowComparisonPanel";
|
||||||
|
import type { WorkflowYAMLConversionResponse } from "../copilot/workflowCopilotTypes";
|
||||||
import "./workspace-styles.css";
|
import "./workspace-styles.css";
|
||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
@@ -1743,105 +1741,133 @@ function Workspace({
|
|||||||
onClose={() => setIsCopilotOpen(false)}
|
onClose={() => setIsCopilotOpen(false)}
|
||||||
onMessageCountChange={setCopilotMessageCount}
|
onMessageCountChange={setCopilotMessageCount}
|
||||||
buttonRef={copilotButtonRef}
|
buttonRef={copilotButtonRef}
|
||||||
onReviewWorkflow={(pendingWorkflow, clearPending) => {
|
onReviewWorkflow={async (pendingWorkflow, clearPending) => {
|
||||||
const saveData = workflowChangesStore.getSaveData?.();
|
const saveData = workflowChangesStore.getSaveData?.();
|
||||||
if (!saveData) return;
|
if (!saveData) return;
|
||||||
|
|
||||||
// Construct fake WorkflowVersion for current state
|
try {
|
||||||
const currentVersion: WorkflowVersion = {
|
// Create YAML from current workflow definition only
|
||||||
workflow_id: saveData.workflow.workflow_id,
|
const workflowDefinitionYaml = convertToYAML({
|
||||||
organization_id: "",
|
version: saveData.workflowDefinitionVersion,
|
||||||
is_saved_task: saveData.workflow.is_saved_task ?? false,
|
parameters: saveData.parameters,
|
||||||
is_template: false,
|
blocks: saveData.blocks,
|
||||||
title: "Current",
|
finally_block_label:
|
||||||
workflow_permanent_id: saveData.workflow.workflow_permanent_id,
|
saveData.settings.finallyBlockLabel ?? undefined,
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Construct fake WorkflowVersion for pending copilot suggestion
|
// Convert current workflow definition YAML to blocks
|
||||||
const pendingVersion: WorkflowVersion = {
|
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||||
...pendingWorkflow,
|
|
||||||
title: "Copilot Suggestion",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle copilot review close with status
|
const currentConversionResponse =
|
||||||
const handleCopilotReviewClose = (status: CopilotReviewStatus) => {
|
await client.post<WorkflowYAMLConversionResponse>(
|
||||||
if (status === "approve") {
|
"/workflow/copilot/convert-yaml-to-blocks",
|
||||||
try {
|
{
|
||||||
applyWorkflowUpdate(pendingWorkflow);
|
workflow_definition_yaml: workflowDefinitionYaml,
|
||||||
} catch (error) {
|
workflow_id: saveData.workflow.workflow_id,
|
||||||
console.error(
|
},
|
||||||
"Failed to apply copilot workflow",
|
);
|
||||||
error,
|
|
||||||
pendingWorkflow,
|
// Construct WorkflowVersion for current state with converted blocks
|
||||||
);
|
const currentVersion: WorkflowVersion = {
|
||||||
toast({
|
workflow_id: saveData.workflow.workflow_id,
|
||||||
title: "Update failed",
|
organization_id: "",
|
||||||
description:
|
is_saved_task: saveData.workflow.is_saved_task ?? false,
|
||||||
"Failed to apply workflow update. Please try again.",
|
is_template: false,
|
||||||
variant: "destructive",
|
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({
|
setWorkflowPanelState({
|
||||||
active: false,
|
active: false,
|
||||||
content: "history",
|
content: "history",
|
||||||
data: {
|
data: {
|
||||||
showComparison: false,
|
version1: currentVersion,
|
||||||
version1: undefined,
|
version2: pendingVersion,
|
||||||
version2: undefined,
|
showComparison: true,
|
||||||
|
mode: "copilot",
|
||||||
|
onCopilotReviewClose: handleCopilotReviewClose,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setIsCopilotOpen(true);
|
} catch (error) {
|
||||||
|
console.error("Failed to prepare workflow comparison", error);
|
||||||
// Clear pending for approve and reject, but not for close
|
toast({
|
||||||
if (status !== "close") {
|
title: "Comparison failed",
|
||||||
clearPending();
|
description:
|
||||||
}
|
"Failed to prepare workflow for comparison. Please try again.",
|
||||||
};
|
variant: "destructive",
|
||||||
|
});
|
||||||
// Hide chat and show comparison
|
}
|
||||||
setIsCopilotOpen(false);
|
|
||||||
setWorkflowPanelState({
|
|
||||||
active: false,
|
|
||||||
content: "history",
|
|
||||||
data: {
|
|
||||||
version1: currentVersion,
|
|
||||||
version2: pendingVersion,
|
|
||||||
showComparison: true,
|
|
||||||
mode: "copilot",
|
|
||||||
onCopilotReviewClose: handleCopilotReviewClose,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onWorkflowUpdate={(workflowData) => {
|
onWorkflowUpdate={(workflowData) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ from skyvern.forge.sdk.schemas.workflow_copilot import (
|
|||||||
WorkflowCopilotStreamErrorUpdate,
|
WorkflowCopilotStreamErrorUpdate,
|
||||||
WorkflowCopilotStreamMessageType,
|
WorkflowCopilotStreamMessageType,
|
||||||
WorkflowCopilotStreamResponseUpdate,
|
WorkflowCopilotStreamResponseUpdate,
|
||||||
|
WorkflowYAMLConversionRequest,
|
||||||
|
WorkflowYAMLConversionResponse,
|
||||||
)
|
)
|
||||||
from skyvern.forge.sdk.services import org_auth_service
|
from skyvern.forge.sdk.services import org_auth_service
|
||||||
from skyvern.forge.sdk.workflow.exceptions import BaseWorkflowHTTPException
|
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 (
|
from skyvern.schemas.workflows import (
|
||||||
LoginBlockYAML,
|
LoginBlockYAML,
|
||||||
WorkflowCreateYAMLRequest,
|
WorkflowCreateYAMLRequest,
|
||||||
|
WorkflowDefinitionYAML,
|
||||||
)
|
)
|
||||||
|
|
||||||
WORKFLOW_KNOWLEDGE_BASE_PATH = Path("skyvern/forge/prompts/skyvern/workflow_knowledge_base.txt")
|
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
|
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):
|
class WorkflowCopilotStreamErrorUpdate(BaseModel):
|
||||||
type: WorkflowCopilotStreamMessageType = Field(WorkflowCopilotStreamMessageType.ERROR, description="Message type")
|
type: WorkflowCopilotStreamMessageType = Field(WorkflowCopilotStreamMessageType.ERROR, description="Message type")
|
||||||
error: str = Field(..., description="Error message")
|
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