Auto-generate meaningful workflow titles via debounced LLM (#SKY-7287) (#4652)
This commit is contained in:
@@ -3,7 +3,6 @@ import { cn } from "@/util/utils";
|
||||
import { AutoResizingTextarea } from "./AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { WorkflowBlockParameterSelect } from "@/routes/workflows/editor/nodes/WorkflowBlockParameterSelect";
|
||||
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
@@ -19,20 +18,12 @@ type Props = Omit<
|
||||
"onChange"
|
||||
> & {
|
||||
aiImprove?: AiImprove;
|
||||
canWriteTitle?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
nodeId: string;
|
||||
};
|
||||
|
||||
function WorkflowBlockInputTextarea(props: Props) {
|
||||
const { maybeAcceptTitle, maybeWriteTitle } = useWorkflowTitleStore();
|
||||
const {
|
||||
aiImprove,
|
||||
nodeId,
|
||||
onChange,
|
||||
canWriteTitle = false,
|
||||
...textAreaProps
|
||||
} = props;
|
||||
const { aiImprove, nodeId, onChange, ...textAreaProps } = props;
|
||||
const [internalValue, setInternalValue] = useState(props.value ?? "");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [cursorPosition, setCursorPosition] = useState<{
|
||||
@@ -46,11 +37,6 @@ function WorkflowBlockInputTextarea(props: Props) {
|
||||
|
||||
const doOnChange = useDebouncedCallback((value: string) => {
|
||||
onChange(value);
|
||||
|
||||
if (canWriteTitle) {
|
||||
maybeWriteTitle(value);
|
||||
maybeAcceptTitle();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handleTextareaSelect = () => {
|
||||
|
||||
@@ -96,6 +96,7 @@ import {
|
||||
import { getWorkflowErrors } from "./workflowEditorUtils";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useAutoPan } from "./useAutoPan";
|
||||
import { useAutoGenerateWorkflowTitle } from "../hooks/useAutoGenerateWorkflowTitle";
|
||||
|
||||
function convertToParametersYAML(
|
||||
parameters: ParametersState,
|
||||
@@ -740,6 +741,7 @@ function FlowRenderer({
|
||||
const editorElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useAutoPan(editorElementRef, nodes);
|
||||
useAutoGenerateWorkflowTitle(nodes, edges);
|
||||
|
||||
useEffect(() => {
|
||||
doLayout(nodes, edges);
|
||||
|
||||
@@ -127,7 +127,6 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
</div>
|
||||
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -121,7 +121,6 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
) : null}
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -233,7 +233,6 @@ function HttpRequestNode({ id, data, type }: NodeProps<HttpRequestNodeType>) {
|
||||
) : null}
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -116,7 +116,6 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
</div>
|
||||
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => update({ url: value })}
|
||||
value={data.url}
|
||||
|
||||
@@ -122,7 +122,6 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
</div>
|
||||
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -125,7 +125,6 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
) : null}
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -88,7 +88,6 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-slate-300">URL</Label>
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -80,7 +80,6 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
|
||||
) : null}
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
update({ url: value });
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import type { Edge } from "@xyflow/react";
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
||||
import { getWorkflowBlocks } from "../editor/workflowEditorUtils";
|
||||
import type { AppNode } from "../editor/nodes";
|
||||
import type { BlockYAML } from "../types/workflowYamlTypes";
|
||||
|
||||
type BlockInfo = {
|
||||
block_type: string;
|
||||
url?: string;
|
||||
goal?: string;
|
||||
};
|
||||
|
||||
function extractBlockInfo(block: BlockYAML): BlockInfo {
|
||||
const info: BlockInfo = { block_type: block.block_type };
|
||||
|
||||
if ("url" in block && block.url) {
|
||||
info.url = block.url;
|
||||
}
|
||||
|
||||
if ("navigation_goal" in block && block.navigation_goal) {
|
||||
info.goal =
|
||||
block.navigation_goal.length > 150
|
||||
? block.navigation_goal.slice(0, 150)
|
||||
: block.navigation_goal;
|
||||
} else if ("data_extraction_goal" in block && block.data_extraction_goal) {
|
||||
info.goal =
|
||||
block.data_extraction_goal.length > 150
|
||||
? block.data_extraction_goal.slice(0, 150)
|
||||
: block.data_extraction_goal;
|
||||
} else if ("prompt" in block && block.prompt) {
|
||||
const prompt = block.prompt;
|
||||
info.goal = prompt.length > 150 ? prompt.slice(0, 150) : prompt;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
function hasMeaningfulContent(blocksInfo: BlockInfo[]): boolean {
|
||||
return blocksInfo.some((b) => b.url || b.goal);
|
||||
}
|
||||
|
||||
const TITLE_GENERATION_DEBOUNCE_MS = 4000;
|
||||
|
||||
function useAutoGenerateWorkflowTitle(nodes: AppNode[], edges: Edge[]): void {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Derive a stable content fingerprint so we only react to actual block
|
||||
// content changes, not to layout/dimension/position updates on nodes.
|
||||
const contentFingerprint = useMemo(() => {
|
||||
const blocks = getWorkflowBlocks(nodes, edges);
|
||||
const info = blocks.slice(0, 5).map(extractBlockInfo);
|
||||
return JSON.stringify(info);
|
||||
}, [nodes, edges]);
|
||||
|
||||
// useDebouncedCallback returns a stable reference (uses useMemo internally
|
||||
// with static deps), so it's safe to call in effects without listing it as
|
||||
// a dependency.
|
||||
const debouncedGenerate = useDebouncedCallback(
|
||||
async (blocksInfo: BlockInfo[]) => {
|
||||
// Re-check title state right before making the API call
|
||||
const state = useWorkflowTitleStore.getState();
|
||||
if (!state.isNewTitle() || state.titleHasBeenGenerated) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any previous in-flight request
|
||||
abortControllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
try {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
const response = await client.post<
|
||||
{ blocks: BlockInfo[] },
|
||||
{ data: { title: string | null } }
|
||||
>(
|
||||
"/prompts/generate-workflow-title",
|
||||
{ blocks: blocksInfo },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
// Re-check after async call - user may have edited title during the request
|
||||
const currentState = useWorkflowTitleStore.getState();
|
||||
if (
|
||||
currentState.isNewTitle() &&
|
||||
!currentState.titleHasBeenGenerated &&
|
||||
response.data.title
|
||||
) {
|
||||
currentState.setTitleFromGeneration(response.data.title);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore - abort errors, network errors, etc.
|
||||
// The first-save fallback in create_workflow_from_request still works.
|
||||
}
|
||||
},
|
||||
TITLE_GENERATION_DEBOUNCE_MS,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const state = useWorkflowTitleStore.getState();
|
||||
|
||||
// Only auto-generate for new, untouched workflows
|
||||
if (!state.isNewTitle() || state.titleHasBeenGenerated) {
|
||||
debouncedGenerate.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const blocksInfo: BlockInfo[] = JSON.parse(contentFingerprint);
|
||||
|
||||
if (!hasMeaningfulContent(blocksInfo)) {
|
||||
debouncedGenerate.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedGenerate(blocksInfo);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contentFingerprint]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedGenerate.cancel();
|
||||
abortControllerRef.current?.abort();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
||||
|
||||
export { useAutoGenerateWorkflowTitle };
|
||||
@@ -1,88 +1,38 @@
|
||||
/**
|
||||
* Context: new workflows begin with a default title. If the user edits a URL
|
||||
* field in a workflow block, and the title is deemed "new", we want to
|
||||
* automagically update the title to the text of the URL. That way, they don't
|
||||
* have to manually update the title themselves, if they deem the automagic
|
||||
* title to be appropriate.
|
||||
*/
|
||||
import { create } from "zustand";
|
||||
|
||||
const DEFAULT_WORKFLOW_TITLE = "New Workflow" as const;
|
||||
const DELIMITER_OPEN = "[[";
|
||||
const DELIMITER_CLOSE = "]]";
|
||||
|
||||
type WorkflowTitleStore = {
|
||||
title: string;
|
||||
/**
|
||||
* If the title is deemed to be new, accept it, and prevent further
|
||||
* `maybeWriteTitle` updates.
|
||||
*/
|
||||
maybeAcceptTitle: () => void;
|
||||
/**
|
||||
* Maybe update the title - if it's empty, or deemed to be new and unedited.
|
||||
*/
|
||||
maybeWriteTitle: (title: string) => void;
|
||||
titleHasBeenGenerated: boolean;
|
||||
isNewTitle: () => boolean;
|
||||
setTitle: (title: string) => void;
|
||||
setTitleFromGeneration: (title: string) => void;
|
||||
initializeTitle: (title: string) => void;
|
||||
resetTitle: () => void;
|
||||
};
|
||||
/**
|
||||
* If the title appears to be a URL, let's trim it down to the domain and path.
|
||||
*/
|
||||
const formatURL = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.hostname + urlObj.pathname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If the title begins and ends with squackets, remove them.
|
||||
*/
|
||||
const formatAcceptedTitle = (title: string) => {
|
||||
if (title.startsWith(DELIMITER_OPEN) && title.endsWith(DELIMITER_CLOSE)) {
|
||||
const trimmed = title.slice(DELIMITER_OPEN.length, -DELIMITER_CLOSE.length);
|
||||
|
||||
return formatURL(trimmed);
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
const formatNewTitle = (title: string) =>
|
||||
title.trim().length
|
||||
? `${DELIMITER_OPEN}${title}${DELIMITER_CLOSE}`
|
||||
: DEFAULT_WORKFLOW_TITLE;
|
||||
|
||||
const isNewTitle = (title: string) =>
|
||||
title === DEFAULT_WORKFLOW_TITLE ||
|
||||
(title.startsWith(DELIMITER_OPEN) && title.endsWith(DELIMITER_CLOSE));
|
||||
|
||||
const useWorkflowTitleStore = create<WorkflowTitleStore>((set, get) => {
|
||||
return {
|
||||
title: "",
|
||||
maybeAcceptTitle: () => {
|
||||
const { title: currentTitle } = get();
|
||||
if (isNewTitle(currentTitle)) {
|
||||
set({ title: formatAcceptedTitle(currentTitle) });
|
||||
}
|
||||
},
|
||||
maybeWriteTitle: (title: string) => {
|
||||
const { title: currentTitle } = get();
|
||||
if (isNewTitle(currentTitle)) {
|
||||
set({ title: formatNewTitle(title.trim()) });
|
||||
}
|
||||
titleHasBeenGenerated: false,
|
||||
isNewTitle: () => {
|
||||
return get().title === DEFAULT_WORKFLOW_TITLE;
|
||||
},
|
||||
setTitle: (title: string) => {
|
||||
set({ title: title.trim() });
|
||||
set({ title: title.trim(), titleHasBeenGenerated: true });
|
||||
},
|
||||
setTitleFromGeneration: (title: string) => {
|
||||
set({ title: title.trim(), titleHasBeenGenerated: true });
|
||||
},
|
||||
initializeTitle: (title: string) => {
|
||||
set({ title: title.trim() });
|
||||
set({
|
||||
title: title.trim(),
|
||||
titleHasBeenGenerated: title.trim() !== DEFAULT_WORKFLOW_TITLE,
|
||||
});
|
||||
},
|
||||
resetTitle: () => {
|
||||
set({ title: "" });
|
||||
set({ title: "", titleHasBeenGenerated: false });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
15
skyvern/forge/prompts/skyvern/generate-workflow-title.j2
Normal file
15
skyvern/forge/prompts/skyvern/generate-workflow-title.j2
Normal file
@@ -0,0 +1,15 @@
|
||||
Generate a brief, descriptive title for a browser automation workflow.
|
||||
|
||||
Rules:
|
||||
- Maximum 5 words
|
||||
- Start with an action verb when possible
|
||||
- Be specific about what the workflow does
|
||||
- Examples: "Scrape LinkedIn job listings", "Extract Amazon product prices", "Submit insurance form"
|
||||
|
||||
Workflow blocks ({{ blocks|length }} total):
|
||||
{% for block in blocks %}
|
||||
- {{ block.block_type }}{% if block.url %} on {{ block.url }}{% endif %}{% if block.goal %}: {{ block.goal }}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
Respond with JSON only:
|
||||
{"title": "generated title here"}
|
||||
@@ -10,8 +10,14 @@ from skyvern.forge.prompts import prompt_engine
|
||||
from skyvern.forge.sdk.api.llm.exceptions import LLMProviderError
|
||||
from skyvern.forge.sdk.routes.routers import base_router
|
||||
from skyvern.forge.sdk.schemas.organizations import Organization
|
||||
from skyvern.forge.sdk.schemas.prompts import ImprovePromptRequest, ImprovePromptResponse
|
||||
from skyvern.forge.sdk.schemas.prompts import (
|
||||
GenerateWorkflowTitleRequest,
|
||||
GenerateWorkflowTitleResponse,
|
||||
ImprovePromptRequest,
|
||||
ImprovePromptResponse,
|
||||
)
|
||||
from skyvern.forge.sdk.services import org_auth_service
|
||||
from skyvern.forge.sdk.workflow.service import generate_title_from_blocks_info
|
||||
|
||||
LOG = structlog.get_logger()
|
||||
|
||||
@@ -124,3 +130,42 @@ async def improve_prompt(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Failed to improve prompt: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@base_router.post(
|
||||
"/prompts/generate-workflow-title",
|
||||
tags=["Prompts"],
|
||||
description="Generate a meaningful workflow title from block content",
|
||||
summary="Generate workflow title",
|
||||
include_in_schema=False,
|
||||
)
|
||||
async def generate_workflow_title(
|
||||
request: GenerateWorkflowTitleRequest,
|
||||
current_org: Organization = Depends(org_auth_service.get_current_org),
|
||||
) -> GenerateWorkflowTitleResponse:
|
||||
"""Generate a meaningful workflow title based on block content using LLM."""
|
||||
LOG.info(
|
||||
"Generating workflow title",
|
||||
organization_id=current_org.organization_id,
|
||||
num_blocks=len(request.blocks),
|
||||
)
|
||||
|
||||
try:
|
||||
blocks_info = [block.model_dump(exclude_none=True) for block in request.blocks]
|
||||
title = await generate_title_from_blocks_info(
|
||||
organization_id=current_org.organization_id,
|
||||
blocks_info=blocks_info,
|
||||
)
|
||||
return GenerateWorkflowTitleResponse(title=title)
|
||||
except LLMProviderError:
|
||||
LOG.error("Failed to generate workflow title", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to generate title. Please try again later.",
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error("Unexpected error generating workflow title", error=str(e), exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Failed to generate title: {str(e)}",
|
||||
)
|
||||
|
||||
@@ -30,3 +30,20 @@ class ImprovePromptResponse(BaseModel):
|
||||
error: str | None = Field(None, description="Error message if prompt improvement failed")
|
||||
improved: str = Field(..., description="The improved version of the prompt")
|
||||
original: str = Field(..., description="The original prompt provided for improvement")
|
||||
|
||||
|
||||
class BlockInfoForTitle(BaseModel):
|
||||
block_type: str = Field(..., description="The type of the workflow block")
|
||||
url: str | None = Field(None, description="URL associated with the block")
|
||||
goal: str | None = Field(None, description="Goal or prompt text for the block")
|
||||
|
||||
|
||||
class GenerateWorkflowTitleRequest(BaseModel):
|
||||
blocks: list[BlockInfoForTitle] = Field(
|
||||
...,
|
||||
description="List of block info objects for title generation",
|
||||
)
|
||||
|
||||
|
||||
class GenerateWorkflowTitleResponse(BaseModel):
|
||||
title: str | None = Field(None, description="The generated workflow title")
|
||||
|
||||
@@ -134,6 +134,76 @@ BLOCK_TYPES_THAT_SHOULD_BE_CACHED = {
|
||||
}
|
||||
|
||||
|
||||
def _extract_blocks_info(blocks: list[BLOCK_YAML_TYPES]) -> list[dict[str, str]]:
|
||||
"""Extract lightweight info from blocks for title generation (limit to first 5)."""
|
||||
blocks_info: list[dict[str, str]] = []
|
||||
for block in blocks[:5]:
|
||||
info: dict[str, str] = {"block_type": block.block_type.value}
|
||||
|
||||
# Extract URL if present
|
||||
if hasattr(block, "url") and block.url:
|
||||
info["url"] = block.url
|
||||
|
||||
# Extract goal/prompt
|
||||
goal = None
|
||||
if hasattr(block, "navigation_goal") and block.navigation_goal:
|
||||
goal = block.navigation_goal
|
||||
elif hasattr(block, "data_extraction_goal") and block.data_extraction_goal:
|
||||
goal = block.data_extraction_goal
|
||||
elif hasattr(block, "prompt") and block.prompt:
|
||||
goal = block.prompt
|
||||
|
||||
if goal:
|
||||
# Truncate long goals
|
||||
info["goal"] = goal[:150] if len(goal) > 150 else goal
|
||||
|
||||
blocks_info.append(info)
|
||||
return blocks_info
|
||||
|
||||
|
||||
async def generate_title_from_blocks_info(
|
||||
organization_id: str,
|
||||
blocks_info: list[dict[str, Any]],
|
||||
) -> str | None:
|
||||
"""Call LLM to generate a workflow title from pre-extracted block info."""
|
||||
if not blocks_info:
|
||||
return None
|
||||
|
||||
try:
|
||||
llm_prompt = prompt_engine.load_prompt(
|
||||
"generate-workflow-title",
|
||||
blocks=blocks_info,
|
||||
)
|
||||
|
||||
response = await app.SECONDARY_LLM_API_HANDLER(
|
||||
prompt=llm_prompt,
|
||||
prompt_name="generate-workflow-title",
|
||||
organization_id=organization_id,
|
||||
)
|
||||
|
||||
if isinstance(response, dict) and "title" in response:
|
||||
title = str(response["title"]).strip()
|
||||
if title and len(title) <= 100: # Sanity check on length
|
||||
return title
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
LOG.exception("Failed to generate workflow title")
|
||||
return None
|
||||
|
||||
|
||||
async def generate_workflow_title(
|
||||
organization_id: str,
|
||||
blocks: list[BLOCK_YAML_TYPES],
|
||||
) -> str | None:
|
||||
"""Generate a meaningful workflow title based on block content using LLM."""
|
||||
if not blocks:
|
||||
return None
|
||||
|
||||
blocks_info = _extract_blocks_info(blocks)
|
||||
return await generate_title_from_blocks_info(organization_id, blocks_info)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheInvalidationPlan:
|
||||
reason: CacheInvalidationReason | None = None
|
||||
@@ -3210,10 +3280,26 @@ class WorkflowService:
|
||||
delete_code_cache_is_ok: bool = True,
|
||||
) -> Workflow:
|
||||
organization_id = organization.organization_id
|
||||
|
||||
# Generate meaningful title if using default and has blocks
|
||||
title = request.title
|
||||
if title == DEFAULT_WORKFLOW_TITLE and request.workflow_definition.blocks:
|
||||
generated_title = await generate_workflow_title(
|
||||
organization_id=organization_id,
|
||||
blocks=request.workflow_definition.blocks,
|
||||
)
|
||||
if generated_title:
|
||||
title = generated_title
|
||||
LOG.info(
|
||||
"Generated workflow title",
|
||||
organization_id=organization_id,
|
||||
generated_title=title,
|
||||
)
|
||||
|
||||
LOG.info(
|
||||
"Creating workflow from request",
|
||||
organization_id=organization_id,
|
||||
title=request.title,
|
||||
title=title,
|
||||
)
|
||||
new_workflow_id: str | None = None
|
||||
|
||||
@@ -3233,7 +3319,7 @@ class WorkflowService:
|
||||
|
||||
# NOTE: it's only potential, as it may be immediately deleted!
|
||||
potential_workflow = await self.create_workflow(
|
||||
title=request.title,
|
||||
title=title,
|
||||
workflow_definition=WorkflowDefinition(parameters=[], blocks=[]),
|
||||
description=request.description,
|
||||
organization_id=organization_id,
|
||||
@@ -3259,7 +3345,7 @@ class WorkflowService:
|
||||
else:
|
||||
# NOTE: it's only potential, as it may be immediately deleted!
|
||||
potential_workflow = await self.create_workflow(
|
||||
title=request.title,
|
||||
title=title,
|
||||
workflow_definition=WorkflowDefinition(parameters=[], blocks=[]),
|
||||
description=request.description,
|
||||
organization_id=organization_id,
|
||||
@@ -3291,7 +3377,7 @@ class WorkflowService:
|
||||
updated_workflow = await self.update_workflow_definition(
|
||||
workflow_id=potential_workflow.workflow_id,
|
||||
organization_id=organization_id,
|
||||
title=request.title,
|
||||
title=title,
|
||||
description=request.description,
|
||||
workflow_definition=workflow_definition,
|
||||
)
|
||||
@@ -3313,7 +3399,7 @@ class WorkflowService:
|
||||
)
|
||||
await self.delete_workflow_by_id(workflow_id=new_workflow_id, organization_id=organization_id)
|
||||
else:
|
||||
LOG.exception(f"Failed to create workflow from request, title: {request.title}")
|
||||
LOG.exception(f"Failed to create workflow from request, title: {title}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user