Workflow Copilot: review and approve/reject changes (#4559)
This commit is contained in:
committed by
GitHub
parent
cb2a72775d
commit
c0f361bb6e
@@ -0,0 +1,33 @@
|
|||||||
|
"""workflow copilot chat proposed workflow
|
||||||
|
|
||||||
|
Revision ID: 43217e31df12
|
||||||
|
Revises: ce791d022652
|
||||||
|
Create Date: 2026-01-27 20:20:36.952383+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "43217e31df12"
|
||||||
|
down_revision: Union[str, None] = "ce791d022652"
|
||||||
|
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_copilot_chats", sa.Column("proposed_workflow", sa.JSON(), nullable=True))
|
||||||
|
op.add_column("workflow_copilot_chats", sa.Column("auto_accept", sa.Boolean(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("workflow_copilot_chats", "auto_accept")
|
||||||
|
op.drop_column("workflow_copilot_chats", "proposed_workflow")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
WorkflowCopilotStreamResponseUpdate,
|
WorkflowCopilotStreamResponseUpdate,
|
||||||
WorkflowCopilotChatSender,
|
WorkflowCopilotChatSender,
|
||||||
WorkflowCopilotChatRequest,
|
WorkflowCopilotChatRequest,
|
||||||
|
WorkflowCopilotClearProposedWorkflowRequest,
|
||||||
} from "./workflowCopilotTypes";
|
} from "./workflowCopilotTypes";
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
@@ -41,7 +42,12 @@ const formatChatTimestamp = (value: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageItem = memo(({ message }: { message: ChatMessage }) => {
|
interface MessageItemProps {
|
||||||
|
message: ChatMessage;
|
||||||
|
footer?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageItem = memo(({ message, footer }: MessageItemProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -55,6 +61,7 @@ const MessageItem = memo(({ message }: { message: ChatMessage }) => {
|
|||||||
<p className="whitespace-pre-wrap pr-3 text-sm text-slate-200">
|
<p className="whitespace-pre-wrap pr-3 text-sm text-slate-200">
|
||||||
{message.content}
|
{message.content}
|
||||||
</p>
|
</p>
|
||||||
|
{footer ? <div className="mt-3 flex gap-2">{footer}</div> : null}
|
||||||
{message.timestamp ? (
|
{message.timestamp ? (
|
||||||
<span className="pointer-events-none absolute bottom-2 right-2 rounded bg-slate-900/70 px-1.5 py-0.5 text-[10px] text-slate-400">
|
<span className="pointer-events-none absolute bottom-2 right-2 rounded bg-slate-900/70 px-1.5 py-0.5 text-[10px] text-slate-400">
|
||||||
{formatChatTimestamp(message.timestamp)}
|
{formatChatTimestamp(message.timestamp)}
|
||||||
@@ -67,6 +74,10 @@ const MessageItem = memo(({ message }: { message: ChatMessage }) => {
|
|||||||
|
|
||||||
interface WorkflowCopilotChatProps {
|
interface WorkflowCopilotChatProps {
|
||||||
onWorkflowUpdate?: (workflow: WorkflowApiResponse) => void;
|
onWorkflowUpdate?: (workflow: WorkflowApiResponse) => void;
|
||||||
|
onReviewWorkflow?: (
|
||||||
|
workflow: WorkflowApiResponse,
|
||||||
|
clearPending: () => void,
|
||||||
|
) => void;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onMessageCountChange?: (count: number) => void;
|
onMessageCountChange?: (count: number) => void;
|
||||||
@@ -117,12 +128,16 @@ const constrainPosition = (
|
|||||||
|
|
||||||
export function WorkflowCopilotChat({
|
export function WorkflowCopilotChat({
|
||||||
onWorkflowUpdate,
|
onWorkflowUpdate,
|
||||||
|
onReviewWorkflow,
|
||||||
isOpen = true,
|
isOpen = true,
|
||||||
onClose,
|
onClose,
|
||||||
onMessageCountChange,
|
onMessageCountChange,
|
||||||
buttonRef,
|
buttonRef,
|
||||||
}: WorkflowCopilotChatProps = {}) {
|
}: WorkflowCopilotChatProps = {}) {
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [proposedWorkflow, setProposedWorkflow] =
|
||||||
|
useState<WorkflowApiResponse | null>(null);
|
||||||
|
const [autoAccept, setAutoAccept] = useState<boolean>(false);
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [processingStatus, setProcessingStatus] = useState<string>("");
|
const [processingStatus, setProcessingStatus] = useState<string>("");
|
||||||
@@ -184,9 +199,74 @@ export function WorkflowCopilotChat({
|
|||||||
const handleNewChat = () => {
|
const handleNewChat = () => {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setWorkflowCopilotChatId(null);
|
setWorkflowCopilotChatId(null);
|
||||||
|
setProposedWorkflow(null);
|
||||||
|
setAutoAccept(false);
|
||||||
hasScrolledOnLoad.current = false;
|
hasScrolledOnLoad.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyWorkflowUpdate = (workflow: WorkflowApiResponse): boolean => {
|
||||||
|
if (!onWorkflowUpdate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
onWorkflowUpdate(workflow);
|
||||||
|
return true;
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error("Failed to update workflow:", updateError);
|
||||||
|
toast({
|
||||||
|
title: "Update failed",
|
||||||
|
description: "Failed to apply workflow changes. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAcceptWorkflow = (
|
||||||
|
workflow: WorkflowApiResponse,
|
||||||
|
alwaysAccept: boolean = false,
|
||||||
|
) => {
|
||||||
|
if (!applyWorkflowUpdate(workflow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProposedWorkflow(null);
|
||||||
|
if (alwaysAccept) {
|
||||||
|
setAutoAccept(true);
|
||||||
|
}
|
||||||
|
void clearProposedWorkflow(alwaysAccept);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRejectWorkflow = () => {
|
||||||
|
setProposedWorkflow(null);
|
||||||
|
void clearProposedWorkflow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearProposedWorkflow = async (autoAcceptValue: boolean) => {
|
||||||
|
try {
|
||||||
|
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||||
|
await client.post<WorkflowCopilotClearProposedWorkflowRequest>(
|
||||||
|
"/workflow/copilot/clear-proposed-workflow",
|
||||||
|
{
|
||||||
|
workflow_copilot_chat_id: workflowCopilotChatId ?? "",
|
||||||
|
auto_accept: autoAcceptValue,
|
||||||
|
} as WorkflowCopilotClearProposedWorkflowRequest,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to clear proposed workflow:", error);
|
||||||
|
toast({
|
||||||
|
title: "Copilot update failed",
|
||||||
|
description: autoAcceptValue
|
||||||
|
? "Workflow was applied, but auto-accept did not update."
|
||||||
|
: "Failed to clear copilot proposal. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReviewWorkflow = (workflow: WorkflowApiResponse) => {
|
||||||
|
onReviewWorkflow?.(workflow, () => setProposedWorkflow(null));
|
||||||
|
};
|
||||||
|
|
||||||
// Notify parent of message count changes
|
// Notify parent of message count changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onMessageCountChange) {
|
if (onMessageCountChange) {
|
||||||
@@ -214,6 +294,8 @@ export function WorkflowCopilotChat({
|
|||||||
if (!workflowPermanentId) {
|
if (!workflowPermanentId) {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setWorkflowCopilotChatId(null);
|
setWorkflowCopilotChatId(null);
|
||||||
|
setProposedWorkflow(null);
|
||||||
|
setAutoAccept(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +325,8 @@ export function WorkflowCopilotChat({
|
|||||||
);
|
);
|
||||||
setMessages(historyMessages);
|
setMessages(historyMessages);
|
||||||
setWorkflowCopilotChatId(response.data.workflow_copilot_chat_id);
|
setWorkflowCopilotChatId(response.data.workflow_copilot_chat_id);
|
||||||
|
setProposedWorkflow(response.data.proposed_workflow ?? null);
|
||||||
|
setAutoAccept(response.data.auto_accept ?? false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load chat history:", error);
|
console.error("Failed to load chat history:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -306,6 +390,7 @@ export function WorkflowCopilotChat({
|
|||||||
|
|
||||||
pendingMessageId.current = userMessageId;
|
pendingMessageId.current = userMessageId;
|
||||||
setMessages((prev) => [...prev, userMessage]);
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
|
setProposedWorkflow(null);
|
||||||
const messageContent = inputValue;
|
const messageContent = inputValue;
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -398,10 +483,7 @@ export function WorkflowCopilotChat({
|
|||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
prev.map((message) =>
|
prev.map((message) =>
|
||||||
message.id === pendingId
|
message.id === pendingId
|
||||||
? {
|
? { ...message, timestamp: payload.timestamp }
|
||||||
...message,
|
|
||||||
timestamp: payload.timestamp,
|
|
||||||
}
|
|
||||||
: message,
|
: message,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -420,19 +502,10 @@ export function WorkflowCopilotChat({
|
|||||||
};
|
};
|
||||||
|
|
||||||
setMessages((prev) => [...prev, aiMessage]);
|
setMessages((prev) => [...prev, aiMessage]);
|
||||||
|
if (response.updated_workflow && autoAccept) {
|
||||||
if (response.updated_workflow && onWorkflowUpdate) {
|
applyWorkflowUpdate(response.updated_workflow);
|
||||||
try {
|
} else {
|
||||||
onWorkflowUpdate(response.updated_workflow);
|
setProposedWorkflow(response.updated_workflow ?? null);
|
||||||
} catch (updateError) {
|
|
||||||
console.error("Failed to update workflow:", updateError);
|
|
||||||
toast({
|
|
||||||
title: "Update failed",
|
|
||||||
description:
|
|
||||||
"Failed to apply workflow changes. Please try again.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -708,9 +781,52 @@ export function WorkflowCopilotChat({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{messages.map((message) => (
|
{messages.map((message, index) => {
|
||||||
<MessageItem key={message.id} message={message} />
|
const isLastMessage = index === messages.length - 1;
|
||||||
))}
|
const showProposedPanel = isLastMessage && proposedWorkflow;
|
||||||
|
return (
|
||||||
|
<MessageItem
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
footer={
|
||||||
|
showProposedPanel ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleReviewWorkflow(proposedWorkflow)}
|
||||||
|
className="rounded border border-blue-500/60 bg-blue-500/10 px-3 py-1 text-xs text-blue-100 hover:bg-blue-500/20"
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleAcceptWorkflow(proposedWorkflow)}
|
||||||
|
className="rounded bg-green-600 px-3 py-1 text-xs text-white hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleAcceptWorkflow(proposedWorkflow, true)
|
||||||
|
}
|
||||||
|
className="rounded bg-emerald-600 px-3 py-1 text-xs text-white hover:bg-emerald-700"
|
||||||
|
>
|
||||||
|
Always accept
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRejectWorkflow}
|
||||||
|
className="rounded bg-red-600 px-3 py-1 text-xs text-white hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white">
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-600 text-xs font-bold text-white">
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ export interface WorkflowCopilotChatHistoryMessage {
|
|||||||
export interface WorkflowCopilotChatHistoryResponse {
|
export interface WorkflowCopilotChatHistoryResponse {
|
||||||
workflow_copilot_chat_id: string | null;
|
workflow_copilot_chat_id: string | null;
|
||||||
chat_history: WorkflowCopilotChatHistoryMessage[];
|
chat_history: WorkflowCopilotChatHistoryMessage[];
|
||||||
|
proposed_workflow?: WorkflowApiResponse | null;
|
||||||
|
auto_accept?: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowCopilotClearProposedWorkflowRequest {
|
||||||
|
workflow_copilot_chat_id: string;
|
||||||
|
auto_accept: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowCopilotStreamMessageType =
|
export type WorkflowCopilotStreamMessageType =
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ 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 { WorkflowSettings } from "../types/workflowTypes";
|
import {
|
||||||
|
Parameter,
|
||||||
|
WorkflowBlock,
|
||||||
|
WorkflowSettings,
|
||||||
|
} from "../types/workflowTypes";
|
||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation } from "@/api/types";
|
||||||
import {
|
import {
|
||||||
nodeAdderNode,
|
nodeAdderNode,
|
||||||
@@ -107,6 +111,7 @@ import {
|
|||||||
import { constructCacheKeyValue, getInitialParameters } from "./utils";
|
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 "./workspace-styles.css";
|
import "./workspace-styles.css";
|
||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
@@ -981,6 +986,40 @@ function Workspace({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyWorkflowUpdate = (workflowData: WorkflowVersion) => {
|
||||||
|
const settings: WorkflowSettings = {
|
||||||
|
proxyLocation: workflowData.proxy_location ?? ProxyLocation.Residential,
|
||||||
|
webhookCallbackUrl: workflowData.webhook_callback_url || "",
|
||||||
|
persistBrowserSession: workflowData.persist_browser_session ?? false,
|
||||||
|
model: workflowData.model ?? null,
|
||||||
|
maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
|
||||||
|
extraHttpHeaders: workflowData.extra_http_headers
|
||||||
|
? JSON.stringify(workflowData.extra_http_headers)
|
||||||
|
: null,
|
||||||
|
runWith: workflowData.run_with ?? null,
|
||||||
|
scriptCacheKey: workflowData.cache_key ?? null,
|
||||||
|
aiFallback: workflowData.ai_fallback ?? true,
|
||||||
|
runSequentially: workflowData.run_sequentially ?? false,
|
||||||
|
sequentialKey: workflowData.sequential_key ?? null,
|
||||||
|
finallyBlockLabel:
|
||||||
|
workflowData.workflow_definition?.finally_block_label ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = getElements(
|
||||||
|
workflowData.workflow_definition.blocks,
|
||||||
|
settings,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
setNodes(elements.nodes);
|
||||||
|
setEdges(elements.edges);
|
||||||
|
|
||||||
|
const initialParameters = getInitialParameters(workflowData);
|
||||||
|
useWorkflowParametersStore.getState().setParameters(initialParameters);
|
||||||
|
|
||||||
|
workflowChangesStore.setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectState = (selectedVersion: WorkflowVersion) => {
|
const handleSelectState = (selectedVersion: WorkflowVersion) => {
|
||||||
// Close panels
|
// Close panels
|
||||||
setWorkflowPanelState({
|
setWorkflowPanelState({
|
||||||
@@ -1239,7 +1278,9 @@ function Workspace({
|
|||||||
<div
|
<div
|
||||||
className="absolute left-6 top-[6rem]"
|
className="absolute left-6 top-[6rem]"
|
||||||
style={{
|
style={{
|
||||||
width: "calc(100% - 32rem)",
|
width: workflowPanelState.active
|
||||||
|
? "calc(100% - 32rem)"
|
||||||
|
: "calc(100% - 3rem)",
|
||||||
height: "calc(100vh - 11rem)",
|
height: "calc(100vh - 11rem)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1248,6 +1289,10 @@ function Workspace({
|
|||||||
version1={workflowPanelState.data.version1}
|
version1={workflowPanelState.data.version1}
|
||||||
version2={workflowPanelState.data.version2}
|
version2={workflowPanelState.data.version2}
|
||||||
onSelectState={handleSelectState}
|
onSelectState={handleSelectState}
|
||||||
|
mode={workflowPanelState.data.mode}
|
||||||
|
onCopilotReviewClose={
|
||||||
|
workflowPanelState.data.onCopilotReviewClose
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1698,43 +1743,109 @@ function Workspace({
|
|||||||
onClose={() => setIsCopilotOpen(false)}
|
onClose={() => setIsCopilotOpen(false)}
|
||||||
onMessageCountChange={setCopilotMessageCount}
|
onMessageCountChange={setCopilotMessageCount}
|
||||||
buttonRef={copilotButtonRef}
|
buttonRef={copilotButtonRef}
|
||||||
|
onReviewWorkflow={(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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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: {
|
||||||
|
version1: currentVersion,
|
||||||
|
version2: pendingVersion,
|
||||||
|
showComparison: true,
|
||||||
|
mode: "copilot",
|
||||||
|
onCopilotReviewClose: handleCopilotReviewClose,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
onWorkflowUpdate={(workflowData) => {
|
onWorkflowUpdate={(workflowData) => {
|
||||||
try {
|
try {
|
||||||
const settings: WorkflowSettings = {
|
applyWorkflowUpdate(workflowData);
|
||||||
proxyLocation:
|
|
||||||
workflowData.proxy_location ?? ProxyLocation.Residential,
|
|
||||||
webhookCallbackUrl: workflowData.webhook_callback_url || "",
|
|
||||||
persistBrowserSession:
|
|
||||||
workflowData.persist_browser_session ?? false,
|
|
||||||
model: workflowData.model ?? null,
|
|
||||||
maxScreenshotScrolls: workflowData.max_screenshot_scrolls || 3,
|
|
||||||
extraHttpHeaders: workflowData.extra_http_headers
|
|
||||||
? JSON.stringify(workflowData.extra_http_headers)
|
|
||||||
: null,
|
|
||||||
runWith: workflowData.run_with ?? null,
|
|
||||||
scriptCacheKey: workflowData.cache_key ?? null,
|
|
||||||
aiFallback: workflowData.ai_fallback ?? true,
|
|
||||||
runSequentially: workflowData.run_sequentially ?? false,
|
|
||||||
sequentialKey: workflowData.sequential_key ?? null,
|
|
||||||
finallyBlockLabel:
|
|
||||||
workflowData.workflow_definition?.finally_block_label ?? null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const elements = getElements(
|
|
||||||
workflowData.workflow_definition.blocks,
|
|
||||||
settings,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
setNodes(elements.nodes);
|
|
||||||
setEdges(elements.edges);
|
|
||||||
|
|
||||||
const initialParameters = getInitialParameters(workflowData);
|
|
||||||
useWorkflowParametersStore
|
|
||||||
.getState()
|
|
||||||
.setParameters(initialParameters);
|
|
||||||
|
|
||||||
workflowChangesStore.setHasChanges(true);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"Failed to parse and apply workflow",
|
"Failed to parse and apply workflow",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
@@ -22,10 +23,16 @@ type BlockComparison = {
|
|||||||
identifier: string;
|
identifier: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ComparisonMode = "history" | "copilot";
|
||||||
|
|
||||||
|
export type CopilotReviewStatus = "approve" | "reject" | "close";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
version1: WorkflowVersion;
|
version1: WorkflowVersion;
|
||||||
version2: WorkflowVersion;
|
version2: WorkflowVersion;
|
||||||
onSelectState?: (version: WorkflowVersion) => void;
|
onSelectState?: (version: WorkflowVersion) => void;
|
||||||
|
mode?: ComparisonMode;
|
||||||
|
onCopilotReviewClose?: (status: CopilotReviewStatus) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mapping from WorkflowBlock.block_type to ReactFlow node.type
|
// Mapping from WorkflowBlock.block_type to ReactFlow node.type
|
||||||
@@ -255,7 +262,13 @@ function WorkflowComparisonRenderer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
function WorkflowComparisonPanel({
|
||||||
|
version1,
|
||||||
|
version2,
|
||||||
|
onSelectState,
|
||||||
|
mode = "history",
|
||||||
|
onCopilotReviewClose,
|
||||||
|
}: Props) {
|
||||||
const comparisons = useMemo(() => {
|
const comparisons = useMemo(() => {
|
||||||
const blocks1 = version1?.workflow_definition?.blocks || [];
|
const blocks1 = version1?.workflow_definition?.blocks || [];
|
||||||
const blocks2 = version2?.workflow_definition?.blocks || [];
|
const blocks2 = version2?.workflow_definition?.blocks || [];
|
||||||
@@ -265,6 +278,20 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
|||||||
version2?.workflow_definition?.blocks,
|
version2?.workflow_definition?.blocks,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ESC key handler for copilot mode - close without rejecting
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode !== "copilot" || !onCopilotReviewClose) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onCopilotReviewClose("close");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [mode, onCopilotReviewClose]);
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
const stats = useMemo(
|
const stats = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -321,7 +348,18 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col rounded-lg bg-slate-elevation2">
|
<div className="flex h-full w-full flex-col rounded-lg bg-slate-elevation2">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 p-4 pb-3">
|
<div className="relative flex-shrink-0 p-4 pb-3">
|
||||||
|
{/* Close button for copilot mode */}
|
||||||
|
{mode === "copilot" && onCopilotReviewClose && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onCopilotReviewClose("close")}
|
||||||
|
className="absolute right-4 top-4 rounded p-1 text-slate-400 hover:bg-slate-700 hover:text-slate-200"
|
||||||
|
title="Close (Esc)"
|
||||||
|
>
|
||||||
|
<Cross2Icon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* 3x3 Grid Layout */}
|
{/* 3x3 Grid Layout */}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{/* Row 1: Workflow Names and Title */}
|
{/* Row 1: Workflow Names and Title */}
|
||||||
@@ -329,17 +367,21 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
|||||||
{version1.title}
|
{version1.title}
|
||||||
</h2>
|
</h2>
|
||||||
<h3 className="text-center text-lg font-medium text-muted-foreground">
|
<h3 className="text-center text-lg font-medium text-muted-foreground">
|
||||||
Version Comparison
|
{mode === "copilot" ? "Copilot Review" : "Version Comparison"}
|
||||||
</h3>
|
</h3>
|
||||||
<h2 className="text-center text-xl font-semibold">
|
<h2 className="text-center text-xl font-semibold">
|
||||||
{version2.title}
|
{version2.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Row 2: Version Details and Statistics */}
|
{/* Row 2: Version Details and Statistics */}
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
{mode === "history" ? (
|
||||||
[Version {version1.version}] •{" "}
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{new Date(version1.modified_at).toLocaleDateString()}
|
[Version {version1.version}] •{" "}
|
||||||
</div>
|
{new Date(version1.modified_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div></div>
|
||||||
|
)}
|
||||||
<div className="flex justify-center gap-3 text-sm">
|
<div className="flex justify-center gap-3 text-sm">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div className="h-3 w-3 rounded-full bg-green-300"></div>
|
<div className="h-3 w-3 rounded-full bg-green-300"></div>
|
||||||
@@ -358,14 +400,18 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
|||||||
<span>Removed ({stats.removed})</span>
|
<span>Removed ({stats.removed})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
{mode === "history" ? (
|
||||||
[Version {version2.version}] •{" "}
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
{new Date(version2.modified_at).toLocaleDateString()}
|
[Version {version2.version}] •{" "}
|
||||||
</div>
|
{new Date(version2.modified_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div></div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Row 3: Select Buttons */}
|
{/* Row 3: Action Buttons */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{onSelectState && (
|
{mode === "history" && onSelectState && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onSelectState(version1)}
|
onClick={() => onSelectState(version1)}
|
||||||
@@ -375,9 +421,29 @@ function WorkflowComparisonPanel({ version1, version2, onSelectState }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div className="flex justify-center gap-2">
|
||||||
|
{mode === "copilot" && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onCopilotReviewClose?.("reject")}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onCopilotReviewClose?.("approve")}
|
||||||
|
className="bg-green-600 text-xs hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{onSelectState && (
|
{mode === "history" && onSelectState && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onSelectState(version2)}
|
onClick={() => onSelectState(version2)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { WorkflowVersion } from "@/routes/workflows/hooks/useWorkflowVersionsQuery";
|
import { WorkflowVersion } from "@/routes/workflows/hooks/useWorkflowVersionsQuery";
|
||||||
|
import { CopilotReviewStatus } from "@/routes/workflows/editor/panels/WorkflowComparisonPanel";
|
||||||
|
|
||||||
export type BranchContext = {
|
export type BranchContext = {
|
||||||
conditionalNodeId: string;
|
conditionalNodeId: string;
|
||||||
@@ -27,6 +28,8 @@ type WorkflowPanelState = {
|
|||||||
version1?: WorkflowVersion;
|
version1?: WorkflowVersion;
|
||||||
version2?: WorkflowVersion;
|
version2?: WorkflowVersion;
|
||||||
showComparison?: boolean;
|
showComparison?: boolean;
|
||||||
|
mode?: "history" | "copilot";
|
||||||
|
onCopilotReviewClose?: (status: CopilotReviewStatus) => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ from skyvern.schemas.workflows import BlockStatus, BlockType, WorkflowStatus
|
|||||||
from skyvern.webeye.actions.actions import Action
|
from skyvern.webeye.actions.actions import Action
|
||||||
|
|
||||||
LOG = structlog.get_logger()
|
LOG = structlog.get_logger()
|
||||||
|
_UNSET = object()
|
||||||
|
|
||||||
|
|
||||||
def _serialize_proxy_location(proxy_location: ProxyLocationInput) -> str | None:
|
def _serialize_proxy_location(proxy_location: ProxyLocationInput) -> str | None:
|
||||||
@@ -3667,6 +3668,33 @@ class AgentDB(BaseAlchemyDB):
|
|||||||
await session.refresh(new_chat)
|
await session.refresh(new_chat)
|
||||||
return WorkflowCopilotChat.model_validate(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(
|
async def create_workflow_copilot_chat_message(
|
||||||
self,
|
self,
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
|
|||||||
@@ -1098,6 +1098,8 @@ class WorkflowCopilotChatModel(Base):
|
|||||||
workflow_copilot_chat_id = Column(String, primary_key=True, default=generate_workflow_copilot_chat_id)
|
workflow_copilot_chat_id = Column(String, primary_key=True, default=generate_workflow_copilot_chat_id)
|
||||||
organization_id = Column(String, nullable=False)
|
organization_id = Column(String, nullable=False)
|
||||||
workflow_permanent_id = Column(String, nullable=False, index=True)
|
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)
|
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||||
modified_at = Column(
|
modified_at = Column(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from skyvern.forge.sdk.schemas.workflow_copilot import (
|
|||||||
WorkflowCopilotChatMessage,
|
WorkflowCopilotChatMessage,
|
||||||
WorkflowCopilotChatRequest,
|
WorkflowCopilotChatRequest,
|
||||||
WorkflowCopilotChatSender,
|
WorkflowCopilotChatSender,
|
||||||
|
WorkflowCopilotClearProposedWorkflowRequest,
|
||||||
WorkflowCopilotProcessingUpdate,
|
WorkflowCopilotProcessingUpdate,
|
||||||
WorkflowCopilotStreamErrorUpdate,
|
WorkflowCopilotStreamErrorUpdate,
|
||||||
WorkflowCopilotStreamMessageType,
|
WorkflowCopilotStreamMessageType,
|
||||||
@@ -448,6 +449,13 @@ async def workflow_copilot_chat_post(
|
|||||||
)
|
)
|
||||||
return
|
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(
|
await app.DATABASE.create_workflow_copilot_chat_message(
|
||||||
organization_id=chat.organization_id,
|
organization_id=chat.organization_id,
|
||||||
workflow_copilot_chat_id=chat.workflow_copilot_chat_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,
|
organization_id=organization.organization_id,
|
||||||
workflow_permanent_id=workflow_permanent_id,
|
workflow_permanent_id=workflow_permanent_id,
|
||||||
)
|
)
|
||||||
if not latest_chat:
|
if latest_chat:
|
||||||
return WorkflowCopilotChatHistoryResponse(workflow_copilot_chat_id=None, chat_history=[])
|
chat_messages = await app.DATABASE.get_workflow_copilot_chat_messages(latest_chat.workflow_copilot_chat_id)
|
||||||
chat_messages = await app.DATABASE.get_workflow_copilot_chat_messages(
|
else:
|
||||||
workflow_copilot_chat_id=latest_chat.workflow_copilot_chat_id,
|
chat_messages = []
|
||||||
)
|
|
||||||
return WorkflowCopilotChatHistoryResponse(
|
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),
|
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(
|
def convert_to_history_messages(
|
||||||
messages: list[WorkflowCopilotChatMessage],
|
messages: list[WorkflowCopilotChatMessage],
|
||||||
) -> list[WorkflowCopilotChatHistoryMessage]:
|
) -> list[WorkflowCopilotChatHistoryMessage]:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class WorkflowCopilotChat(BaseModel):
|
|||||||
workflow_copilot_chat_id: str = Field(..., description="ID for the workflow copilot chat")
|
workflow_copilot_chat_id: str = Field(..., description="ID for the workflow copilot chat")
|
||||||
organization_id: str = Field(..., description="Organization ID for the chat")
|
organization_id: str = Field(..., description="Organization ID for the chat")
|
||||||
workflow_permanent_id: str = Field(..., description="Workflow permanent 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")
|
created_at: datetime = Field(..., description="When the chat was created")
|
||||||
modified_at: datetime = Field(..., description="When the chat was last modified")
|
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")
|
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):
|
class WorkflowCopilotChatHistoryMessage(BaseModel):
|
||||||
sender: WorkflowCopilotChatSender = Field(..., description="Message sender")
|
sender: WorkflowCopilotChatSender = Field(..., description="Message sender")
|
||||||
content: str = Field(..., description="Message content")
|
content: str = Field(..., description="Message content")
|
||||||
@@ -49,6 +56,8 @@ class WorkflowCopilotChatHistoryMessage(BaseModel):
|
|||||||
class WorkflowCopilotChatHistoryResponse(BaseModel):
|
class WorkflowCopilotChatHistoryResponse(BaseModel):
|
||||||
workflow_copilot_chat_id: str | None = Field(None, description="Latest chat ID for the workflow")
|
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")
|
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):
|
class WorkflowCopilotStreamMessageType(StrEnum):
|
||||||
|
|||||||
Reference in New Issue
Block a user