From 6a809bdca031ccdce61874314da8444e3670412f Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Wed, 13 Sep 2023 14:24:27 +0200 Subject: [PATCH] feat: improve ai flow (#2270) * feat: improve ai flow * fix: copilot status popup placement * fix: step only approve flow inputs additions + nits * fix: nits --- .../src/lib/components/FlowBuilder.svelte | 370 +++++++++++------- .../src/lib/components/ManualPopover.svelte | 6 +- frontend/src/lib/components/Modal.svelte | 110 ------ .../lib/components/common/modal/Modal.svelte | 1 + .../copilot/FlowCopilotDrawer.svelte | 143 +++++-- .../copilot/FlowCopilotInputsModal.svelte | 39 ++ .../copilot/FlowCopilotStatus.svelte | 36 +- .../lib/components/copilot/ScriptFix.svelte | 45 ++- .../lib/components/copilot/ScriptGen.svelte | 9 +- .../src/lib/components/copilot/StepGen.svelte | 184 +++++++++ frontend/src/lib/components/copilot/flow.ts | 163 +++++--- .../flows/map/InsertModuleButton.svelte | 193 ++++----- .../lib/components/flows/map/MapItem.svelte | 4 + .../components/flows/map/VirtualItem.svelte | 77 ++-- .../lib/components/flows/previousResults.ts | 39 +- 15 files changed, 899 insertions(+), 520 deletions(-) delete mode 100644 frontend/src/lib/components/Modal.svelte create mode 100644 frontend/src/lib/components/copilot/FlowCopilotInputsModal.svelte create mode 100644 frontend/src/lib/components/copilot/StepGen.svelte diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 955ec9e578..ba006908be 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -6,17 +6,23 @@ type FlowModule, DraftService, type PathScript, - RawScript, - ScriptService + ScriptService, + Script } from '$lib/gen' - import { initHistory, redo, undo } from '$lib/history' - import { enterpriseLicense, hubScripts, userStore, workspaceStore } from '$lib/stores' + import { initHistory, push, redo, undo } from '$lib/history' + import { + enterpriseLicense, + existsOpenaiResourcePath, + hubScripts, + userStore, + workspaceStore + } from '$lib/stores' import { encodeState, formatCron, sleep } from '$lib/utils' import { sendUserToast } from '$lib/toast' import type { Drawer } from '$lib/components/common' import { faCalendarAlt, faSave } from '@fortawesome/free-solid-svg-icons' - import { setContext } from 'svelte' + import { setContext, tick } from 'svelte' import { writable, type Writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Badge, Button, Kbd, UndoRedo } from './common' @@ -24,12 +30,13 @@ import FlowEditor from './flows/FlowEditor.svelte' import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte' import type { FlowState } from './flows/flowState' - import { dfs } from './flows/flowStore' + import { dfs as dfsApply } from './flows/flowStore' + import { dfs, getPreviousIds } from './flows/previousResults' import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte' import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte' import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils' import type { FlowEditorContext } from './flows/types' - import { cleanInputs } from './flows/utils' + import { cleanInputs, emptyFlowModuleState } from './flows/utils' import { Pen } from 'lucide-svelte' import { loadHubScripts } from '$lib/scripts' import { createEventDispatcher } from 'svelte' @@ -41,11 +48,12 @@ glueCopilot, type FlowCopilotContext } from './copilot/flow' - import { numberToChars } from './flows/idUtils' import type { Schema, SchemaProperty } from '$lib/common' import FlowCopilotDrawer from './copilot/FlowCopilotDrawer.svelte' import FlowCopilotStatus from './copilot/FlowCopilotStatus.svelte' import { fade } from 'svelte/transition' + import { loadFlowModuleState } from './flows/flowStateUtils' + import FlowCopilotInputsModal from './copilot/FlowCopilotInputsModal.svelte' export let initialPath: string = '' export let selectedId: string | undefined @@ -342,7 +350,7 @@ return [ 'settings-metadata', 'constants', - ...dfs($flowStore.value.modules, (module) => module.id) + ...dfsApply($flowStore.value.modules, (module) => module.id) ] } @@ -366,7 +374,8 @@ let flowCopilotContext: FlowCopilotContext = { drawerStore: writable(undefined), modulesStore: writable([]), - currentStepStore: writable(undefined) + currentStepStore: writable(undefined), + genFlow: undefined } setContext('FlowCopilotContext', flowCopilotContext) @@ -378,7 +387,7 @@ } = flowCopilotContext let doneTs = 0 - async function hubCompletions(text: string, idx: number, type: 'trigger' | 'script') { + async function getHubCompletions(text: string, idx: number, type: 'trigger' | 'script') { try { // make sure we display the results of the last request last const ts = Date.now() @@ -414,9 +423,12 @@ let copilotLoading = false let flowCopilotMode: 'trigger' | 'sequence' = 'trigger' let copilotStatus: string = '' + let copilotFlowInputs: Record = {} + let copilotFlowRequiredInputs: string[] = [] + let openCopilotInputsModal = false - function getInitCopilotModules(mode: typeof flowCopilotMode): FlowCopilotModule[] { - return [ + function setInitCopilotModules(mode: typeof flowCopilotMode) { + $copilotModulesStore = [ { id: 'a', type: mode === 'trigger' ? 'trigger' : 'script', @@ -424,7 +436,8 @@ code: '', hubCompletions: [], selectedCompletion: undefined, - source: undefined + source: undefined, + lang: undefined }, { id: 'b', @@ -433,38 +446,60 @@ code: '', hubCompletions: [], selectedCompletion: undefined, - source: undefined + source: undefined, + lang: undefined } ] } - $: { - copilotModulesStore.set(getInitCopilotModules(flowCopilotMode)) + $: setInitCopilotModules(flowCopilotMode) + + function applyCopilotFlowInputs() { + const properties = { + ...($flowStore.schema?.properties as Record | undefined), + ...copilotFlowInputs + } + const required = [ + ...(($flowStore.schema?.required as string[] | undefined) ?? []), + ...copilotFlowRequiredInputs + ] + $flowStore.schema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties, + required, + type: 'object' + } + copilotFlowInputs = {} + copilotFlowRequiredInputs = [] } - async function genFlow(i: number) { - copilotLoading = true - copilotStatus = "Generating code for step '" + numberToChars(i) + "'..." - $copilotCurrentStepStore = numberToChars(i) + async function genFlow(idx: number, flowModules: FlowModule[], stepOnly = false) { try { - abortController = new AbortController() + push(history, $flowStore) + let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx] - $flowStore.value.modules = $flowStore.value.modules.slice(0, i) - let prevCode = '' - if (i === 0) { - prevCode = '' + copilotLoading = true + copilotStatus = "Generating code for step '" + module.id + "'..." + $copilotCurrentStepStore = module.id + focusCopilot() + + if (!stepOnly && flowModules.length > idx) { + select('') + await tick() + flowModules.splice(idx, flowModules.length - idx) + $flowStore = $flowStore + focusCopilot() + } + + if (idx === 0 && !stepOnly) { $flowStore.schema = { $schema: 'https://json-schema.org/draft/2020-12/schema', properties: {}, required: [], type: 'object' } - } else { - prevCode = ($flowStore.value.modules[i - 1].value as RawScript).content } - let module = $copilotModulesStore[i] - if (module.type === 'trigger') { if (!$scheduleStore.cron) { $scheduleStore.cron = '0 */15 * * *' @@ -472,8 +507,24 @@ $scheduleStore.enabled = true } + let hubScript: + | { + content: string + lockfile?: string | undefined + schema?: any + language: string + summary?: string | undefined + } + | undefined = undefined + + if (module.source === 'hub' && module.selectedCompletion) { + hubScript = await ScriptService.getHubScriptByPath({ + path: module.selectedCompletion.path + }) + } + const flowModule = { - id: numberToChars(i), + id: module.id, stop_after_if: module.type === 'trigger' ? { @@ -484,16 +535,18 @@ value: { input_transforms: {}, content: '', - language: RawScript.language.BUN, + language: (hubScript ? hubScript.language : module.lang ?? 'bun') as Script.language, type: 'rawscript' as const }, - summary: - $copilotModulesStore[i].selectedCompletion?.summary ?? $copilotModulesStore[i].description + summary: module.selectedCompletion?.summary ?? module.description } - if (i === 1 && $copilotModulesStore[i - 1].type === 'trigger') { + $flowStateStore[module.id] = emptyFlowModuleState() + if (stepOnly) { + flowModules.splice(idx, 0, flowModule) + } else if (idx === 1 && $copilotModulesStore[idx - 1].type === 'trigger') { const loopModule: FlowModule = { - id: numberToChars(i) + '_loop', + id: module.id + '_loop', value: { type: 'forloopflow', iterator: { @@ -504,152 +557,167 @@ modules: [flowModule] } } - - $flowStore.value.modules.push(loopModule) + const loopState = await loadFlowModuleState(loopModule) + $flowStateStore[loopModule.id] = loopState + flowModules.push(loopModule) } else { - $flowStore.value.modules.push(flowModule) + flowModules.push(flowModule) } $copilotDrawerStore?.closeDrawer() - select(numberToChars(i)) - await sleep(200) + select(module.id) + await tick() + focusCopilot() - $copilotModulesStore[i].editor?.setCode('') - const deltaStore = writable('') - const unsubscribe = deltaStore.subscribe(async (delta) => { - $copilotModulesStore[i].editor?.append(delta) - }) - await stepCopilot(module, deltaStore, prevCode, abortController) - unsubscribe() + let isFirstInLoop = false + const parents = dfs(module.id, $flowStore).slice(1) + if ( + parents[0]?.value.type === 'forloopflow' && + parents[0].value.modules[0].id === module.id + ) { + isFirstInLoop = true + } + const prevNodeId = getPreviousIds(module.id, $flowStore, false)[0] + const pastModule: FlowModule | undefined = dfs(prevNodeId, $flowStore, false)[0] - copilotStatus = "Generating inputs for step '" + numberToChars(i) + "'..." + if (hubScript) { + module.editor?.setCode(hubScript.content) + } else if (module.source === 'custom') { + module.editor?.setCode('') + const deltaStore = writable('') + const unsubscribe = deltaStore.subscribe(async (delta) => { + module.editor?.append(delta) + }) + + abortController = new AbortController() + await stepCopilot( + module, + deltaStore, + pastModule?.value.type === 'rawscript' ? pastModule.value.content : '', + pastModule?.value.type === 'rawscript' ? pastModule.value.language : undefined, + pastModule === undefined, + isFirstInLoop, + abortController + ) + unsubscribe() + } else { + throw new Error('Invalid copilot module source') + } + + copilotStatus = "Generating inputs for step '" + module.id + "'..." await sleep(500) // make sure code was parsed try { - let currentFlowModule = $flowStore.value.modules[i] - if (currentFlowModule.value.type === 'forloopflow') { - currentFlowModule = currentFlowModule.value.modules[0] - } - - if (currentFlowModule.value.type === 'rawscript') { + if (flowModule.value.type === 'rawscript') { const stepSchema: Schema = JSON.parse(JSON.stringify($flowStateStore[module.id].schema)) // deep copy - if (module.source === 'hub' && i >= 1) { + if (module.source === 'hub' && pastModule !== undefined && $existsOpenaiResourcePath) { // ask AI to set step inputs - const pastModule = $flowStore.value.modules[i - 1] + abortController = new AbortController() const inputs = await glueCopilot( - Object.keys(currentFlowModule.value.input_transforms), + Object.keys(flowModule.value.input_transforms), pastModule.value.type === 'rawscript' ? pastModule.value.content : '', - i === 1 && $copilotModulesStore[i - 1].type === 'trigger', + pastModule.value.type === 'rawscript' ? pastModule.value.language : undefined, + isFirstInLoop, abortController ) // create flow inputs used by AI for autocompletion - Object.entries(inputs) - .filter( - ([key, expr]) => - key in stepSchema.properties && - expr.startsWith('flow_inputs.') && - !expr.startsWith('flow_inputs.iter') - ) - .map(([key, _]) => { - const inputSchemaProperty = stepSchema.properties[key] - const isRequired = stepSchema.required.includes(key) - - if ($flowStore.schema) { - $flowStore.schema.properties[key] = inputSchemaProperty - if (isRequired) { - $flowStore.schema.required.push(key) - } - } else { - $flowStore.schema = { - $schema: 'https://json-schema.org/draft/2020-12/schema', - properties: { - [key]: inputSchemaProperty - }, - required: isRequired ? [key] : [], - type: 'object' - } + copilotFlowInputs = {} + copilotFlowRequiredInputs = [] + Object.entries(inputs).forEach(([key, expr]) => { + if ( + key in stepSchema.properties && + expr.startsWith('flow_input.') && + !expr.startsWith('flow_input.iter') && + (!$flowStore.schema || !(key in $flowStore.schema.properties)) // prevent overriding flow inputs + ) { + copilotFlowInputs[key] = stepSchema.properties[key] + if (stepSchema.required.includes(key)) { + copilotFlowRequiredInputs.push(key) } - $flowStore.schema - }) - - flowModule.value.input_transforms = Object.entries(inputs).reduce( - (acc, [key, expr]) => { - acc[key] = { - type: 'javascript', - expr - } - return acc - }, - {} - ) - } else { - // create possible flow inputs for autocompletion - delete stepSchema.properties.prev_output - $flowStore.schema = { - $schema: 'https://json-schema.org/draft/2020-12/schema', - properties: { - ...$flowStore.schema?.properties, - ...stepSchema.properties - }, - required: Array.from( - new Set([...$flowStore.schema?.required, ...stepSchema.required]) - ), - type: 'object' + } + }) + if (!stepOnly) { + applyCopilotFlowInputs() } - // programatically set step inputs - for (const key of Object.keys(currentFlowModule.value.input_transforms)) { + // set step inputs + Object.entries(inputs).forEach(([key, expr]) => { + flowModule.value.input_transforms[key] = { + type: 'javascript', + expr + } + }) + } else { + if (module.source === 'hub' && pastModule !== undefined && !$existsOpenaiResourcePath) { + sendUserToast( + 'For better input generation, enable Windmill AI in the workspace settings', + true + ) + } + + // create possible flow inputs for autocompletion + copilotFlowInputs = {} + copilotFlowRequiredInputs = [] + Object.keys(flowModule.value.input_transforms).forEach((key) => { if (key !== 'prev_output') { const schema = $flowStateStore[module.id].schema const schemaProperty = Object.entries(schema.properties).find( (x) => x[0] === key )?.[1] - if (schemaProperty) { - $flowStore.schema = { - $schema: 'https://json-schema.org/draft/2020-12/schema', - properties: { - ...$flowStore.schema?.properties, - [key]: schemaProperty - }, - required: schemaProperty.required - ? Array.from(new Set([...$flowStore.schema?.required, key])) - : $flowStore.schema?.required, - type: 'object' + if ( + schemaProperty && + (!$flowStore.schema || !(key in $flowStore.schema.properties)) // prevent overriding flow inputs + ) { + copilotFlowInputs[key] = schemaProperty + if (schema.required.includes(key)) { + copilotFlowRequiredInputs.push(key) } } } + }) + if (!stepOnly) { + applyCopilotFlowInputs() + } + // programatically set step inputs + for (const key of Object.keys(flowModule.value.input_transforms)) { flowModule.value.input_transforms[key] = { type: 'javascript', expr: key === 'prev_output' - ? $copilotModulesStore[i - 1].type === 'trigger' + ? isFirstInLoop ? 'flow_input.iter.value' - : 'results.' + $copilotModulesStore[i - 1].id + : pastModule + ? 'results.' + pastModule.id + : 'flow_input.' + key : 'flow_input.' + key } } } - const wrappingFlowModule = $flowStore.value.modules[i] - if (wrappingFlowModule.value.type === 'forloopflow') { - wrappingFlowModule.value = { - ...wrappingFlowModule.value, - modules: [flowModule] - } - $flowStore.value.modules[i] = wrappingFlowModule - } else { - $flowStore.value.modules[i] = flowModule - } + $flowStore = $flowStore // force rerendering } } catch (err) { console.error(err) } - copilotStatus = - "Waiting for the user to validate code and inputs of step '" + numberToChars(i) + "'" + if (stepOnly) { + openCopilotInputsModal = true + $copilotCurrentStepStore = undefined + copilotLoading = false + setInitCopilotModules(flowCopilotMode) + copilotStatus = '' + } else { + copilotStatus = + "Waiting for the user to validate code and inputs of step '" + module.id + "'" + } } catch (err) { + if (stepOnly) { + copilotStatus = '' + $copilotCurrentStepStore = undefined + setInitCopilotModules(flowCopilotMode) + } if (err?.message) { sendUserToast('Failed to generate code: ' + err.message, true) } else { @@ -661,7 +729,9 @@ } } - async function handleFlowGenInputs() { + flowCopilotContext.genFlow = genFlow + + async function handleFlowCopilotInputs() { copilotLoading = true select('Input') $copilotCurrentStepStore = 'Input' @@ -734,14 +804,30 @@ }) } - $: $copilotCurrentStepStore !== undefined ? focusCopilot() : blurCopilot() + $: $copilotCurrentStepStore === undefined && blurCopilot() - - {#if !$userStore?.operator} + + { + applyCopilotFlowInputs() + copilotStatus = "Done! Just check the step's inputs and you're good to go!" + await sleep(3000) + copilotStatus = '' + }} + on:canceled={async () => { + copilotFlowInputs = {} + copilotFlowRequiredInputs = [] + copilotStatus = "Done! Just check the step's inputs and you're good to go!" + await sleep(3000) + copilotStatus = '' + }} + bind:open={openCopilotInputsModal} + inputs={Object.keys(copilotFlowInputs)} + />
@@ -825,7 +911,7 @@ {copilotLoading} bind:copilotStatus {genFlow} - {handleFlowGenInputs} + {handleFlowCopilotInputs} {abortController} /> diff --git a/frontend/src/lib/components/ManualPopover.svelte b/frontend/src/lib/components/ManualPopover.svelte index 7c70926433..a021fcb7c2 100644 --- a/frontend/src/lib/components/ManualPopover.svelte +++ b/frontend/src/lib/components/ManualPopover.svelte @@ -6,7 +6,11 @@ export let placement: PopoverPlacement = 'bottom' - const [popperRef, popperContent] = createPopperActions({ placement }) + const [popperRef, popperContent, getInstance] = createPopperActions({ placement }) + + export async function refresh() { + await getInstance()?.update() + } let showTooltip = false export function open() { diff --git a/frontend/src/lib/components/Modal.svelte b/frontend/src/lib/components/Modal.svelte deleted file mode 100644 index 775ea7ec08..0000000000 --- a/frontend/src/lib/components/Modal.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - - - -{#if open} -
- -
-
- {#if open} -
- -

-
-
-
- -
-
-
- -   -
-
- {/if} -
-
-{/if} - - diff --git a/frontend/src/lib/components/common/modal/Modal.svelte b/frontend/src/lib/components/common/modal/Modal.svelte index db01d95469..74e91ac6dd 100644 --- a/frontend/src/lib/components/common/modal/Modal.svelte +++ b/frontend/src/lib/components/common/modal/Modal.svelte @@ -76,6 +76,7 @@
+
@@ -96,13 +117,13 @@ - {#if $currentStepStore !== undefined && $currentStepStore !== 'Input' && i < charsToNumber($currentStepStore)} + {#if $currentStepStore !== undefined && i < charsToNumber($currentStepStore)}

Already generated, edit step to regenerate from this point

@@ -125,7 +146,7 @@ bind:value={copilotModule.description} on:input={() => { if (copilotModule.description.length > 2) { - hubCompletions(copilotModule.description, i, copilotModule.type) + getHubCompletions(copilotModule.description, i, copilotModule.type) } else { copilotModule.hubCompletions = [] } @@ -133,27 +154,58 @@ /> {/if} {#if copilotModule.description.length > 2 && copilotModule.source === undefined} - + + + {#if copilotModule.hubCompletions.length > 0}

Hub scripts

    @@ -162,8 +214,12 @@ @@ -216,14 +273,14 @@ diff --git a/frontend/src/lib/components/copilot/FlowCopilotInputsModal.svelte b/frontend/src/lib/components/copilot/FlowCopilotInputsModal.svelte new file mode 100644 index 0000000000..1450e533f6 --- /dev/null +++ b/frontend/src/lib/components/copilot/FlowCopilotInputsModal.svelte @@ -0,0 +1,39 @@ + + + { + open = false + dispatch('confirmed') + }} + on:canceled + title="Windmill AI wants to add the following inputs to the flow:" +> +
      + {#each inputs as input} +
    • {input}
    • + {/each} +
    + + +
    diff --git a/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte b/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte index 8e8ee90df6..1c0c8a320b 100644 --- a/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte +++ b/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte @@ -8,19 +8,26 @@ import { charsToNumber } from '../flows/idUtils' import { existsOpenaiResourcePath } from '$lib/stores' import Popup from '../common/popup/Popup.svelte' + import type { FlowModule } from '$lib/gen' + import type { FlowEditorContext } from '../flows/types' + import { ExternalLink } from 'lucide-svelte' export let copilotLoading: boolean export let copilotStatus: string export let abortController: AbortController | undefined - export let genFlow: (index: number) => void - export let handleFlowGenInputs: () => void + export let genFlow: (index: number, modules: FlowModule[], stepOnly?: boolean) => void + export let handleFlowCopilotInputs: () => void let copilotPopover: ManualPopover | undefined = undefined + const { flowStore } = getContext('FlowEditorContext') + const { modulesStore, drawerStore, currentStepStore } = getContext('FlowCopilotContext') $: copilotStatus.length > 0 ? copilotPopover?.open() : copilotPopover?.close() + + $: copilotStatus && copilotPopover?.refresh() {#if $existsOpenaiResourcePath} @@ -62,8 +69,8 @@ ? 'Exit' : 'AI Flow Builder'} -
    +
    + {copilotStatus} {#if !copilotLoading && $currentStepStore !== undefined && $currentStepStore !== 'Input'} @@ -85,9 +92,9 @@ } const stepNb = charsToNumber($currentStepStore) if (stepNb >= $modulesStore.length - 1) { - handleFlowGenInputs() + handleFlowCopilotInputs() } else { - genFlow(stepNb + 1) + genFlow(stepNb + 1, $flowStore.value.modules) } }} > @@ -95,11 +102,16 @@ ? 'Flow inputs' : 'Next step'} - {/if}
    + {/if} +
    {:else} - + {@const fixAction = (_) => { - onFix(() => close(null)) + if ($existsOpenaiResourcePath) { + onFix(() => close(null)) + } }}
    -
    - {#if $generatedCode.length > 0} -
    - -
    - {#if $generatedExplanation.length > 0} -

    Explanation: {$generatedExplanation}

    + {#if $existsOpenaiResourcePath} +
    + {#if $generatedCode.length > 0} +
    + +
    + {#if $generatedExplanation.length > 0} +

    Explanation: {$generatedExplanation}

    + {/if} + {:else} + {/if} - {:else} - - {/if} -
    - {#if !$existsOpenaiResourcePath} -

    Enable Windmill AI in the workspace settings.

    + {:else} +
    +

    Enable Windmill AI in the workspace settings

    {/if}
    diff --git a/frontend/src/lib/components/copilot/ScriptGen.svelte b/frontend/src/lib/components/copilot/ScriptGen.svelte index 3cf19804fd..9b998f80ed 100644 --- a/frontend/src/lib/components/copilot/ScriptGen.svelte +++ b/frontend/src/lib/components/copilot/ScriptGen.svelte @@ -27,6 +27,7 @@ import LoadingIcon from '../apps/svelte-select/lib/LoadingIcon.svelte' import { sleep } from '$lib/utils' import { autoPlacement } from '@floating-ui/core' + import { ExternalLink } from 'lucide-svelte' // props export let iconOnly: boolean = false @@ -309,8 +310,12 @@ {/if} {:else}

    Enable Windmill AI in the workspace settings.

    Enable Windmill AI in the workspace settings +

    {/if}
    diff --git a/frontend/src/lib/components/copilot/StepGen.svelte b/frontend/src/lib/components/copilot/StepGen.svelte new file mode 100644 index 0000000000..064031aedd --- /dev/null +++ b/frontend/src/lib/components/copilot/StepGen.svelte @@ -0,0 +1,184 @@ + + +
    +
    +
    + { + if (funcDesc.length > 2) { + getHubCompletions(funcDesc) + } else { + hubCompletions = [] + } + }} + placeholder="AI Gen  or search hub scripts" + style="font-family:Inter, FontAwesome" + /> +
    + {#if funcDesc.length > 0} +
      +
    • + +
    • +
    • + +
    • +
    + {/if} + {#if hubCompletions.length > 0} +
    +

    Hub Scripts

    +
      + {#each hubCompletions as item (item.path)} +
    • + +
    • + {/each} +
    +
    + {/if} +
    +
    diff --git a/frontend/src/lib/components/copilot/flow.ts b/frontend/src/lib/components/copilot/flow.ts index def352a99c..5a6d61f09d 100644 --- a/frontend/src/lib/components/copilot/flow.ts +++ b/frontend/src/lib/components/copilot/flow.ts @@ -1,8 +1,9 @@ -import { ScriptService, type Script } from '$lib/gen' +import type { Script, FlowModule } from '$lib/gen' import { addResourceTypes, deltaCodeCompletion, getNonStreamingCompletion } from './lib' import type { Writable } from 'svelte/store' import type Editor from '../Editor.svelte' import type { Drawer } from '../common' +import { scriptLangToEditorLang } from '$lib/scripts' export type FlowCopilotModule = { id: string @@ -10,6 +11,7 @@ export type FlowCopilotModule = { description: string code: string source: 'hub' | 'custom' | undefined + lang: 'bun' | 'python3' | undefined hubCompletions: { path: string summary: string @@ -35,6 +37,7 @@ export type FlowCopilotContext = { drawerStore: Writable modulesStore: Writable currentStepStore: Writable + genFlow: ((i: number, modules: FlowModule[], stepOnly?: boolean) => Promise) | undefined } const systemPrompt = `You write code as instructed by the user. Only output code. Wrap the code in a code block. @@ -42,35 +45,67 @@ Put explanations directly in the code as comments. Here's how interactions have to look like: user: {sample_question} -assistant: \`\`\`typescript +assistant: \`\`\`{codeLang} {code} \`\`\`` -const additionalInformation = `Additional information: We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it. +const additionalInfos: { + bun: string + python3: string +} = { + bun: `Additional information: We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it. You have access to the following resource types, if you need them, you have to define the type exactly as specified and add them as parameters: {resourceTypes} -Only use the ones you need. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.` +Only use the ones you need. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.`, + python3: `Additional information: We have to export a "main" function and specify the parameter types but do not call it. +You have access to the following resource types, if you need them, you have to define the TypedDict exactly as specified (class name has to be IN LOWERCASE) and add them as parameters: {resourceTypes} +Only use the ones you need. If the TypedDict name conflicts with the imported object, rename the imported object NOT THE TYPE.` +} -const triggerPrompt = `I'm building a workflow which is a sequence of script steps. Write the first script in typescript which should check for {description} and return an array. +const triggerPrompts: { + bun: string + python3: string +} = { + bun: `I'm building a workflow which is a sequence of script steps. Write the first script in {codeLang} which should check for {description} and return an array. You can use "const {state_name}: {state_type} = getState(...)" and "setState(...)" from "npm:windmill-client@1" to maintain state across runs. -${additionalInformation}` +{additionalInformation}`, + python3: `I'm building a workflow which is a sequence of script steps. Write the first script in {codeLang} which should check for {description} and return an array. +You can use get_state and set_state from wmill to maintain state across runs. -const firstActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in typescript which should {description}. +{additionalInformation}` +} + +const firstActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}. Return the script's output. -${additionalInformation}` +{additionalInformation}` -const actionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in typescript which should {description} using as a parameter called "prev_output" the output of the previous script. -Infer the type of "prev_output" from the previous script: \`\`\`typescript\n{prevCode}\n\`\`\`. +const inferTypePrompt = + 'Infer the type of "prev_output" from the previous\'s step code: ```{codeLang}\n{prevCode}\n```' + +const actionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}. It should take a parameter called "prev_output" which contains the output of the previous script. +{inferTypePrompt} Return the script's output. -${additionalInformation}` +{additionalInformation}` + +const inferTypeLoopPrompt = + 'Infer the type of "prev_output" from the previous\'s step code: ```{codeLang}\n{prevCode}\n```, keeping in mind that it is ONE ELEMENT of the output of the previous step.' + +const loopActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}. It should take a parameter called "prev_output" which contains ONE ELEMEMT of the output of the previous script. +{inferTypePrompt} +Return the script's output. + +{additionalInformation}` + +const inferTypeGluePrompt = + "Infer its type from the previous's step code: ```{codeLang}\n{prevCode}\n```" const loopGluePrompt = `I'm building a workflow which is a sequence of script steps. My current step code has the following inputs: {inputs}. Determine what to pass as inputs. You can only use the following: - \`flow_input\` (javascript object): general inputs that are passed to the workflow, you can assume any object properties. -- \`flow_input.iter.value\` (javascript object): it is ONE ELEMENT of the output of the previous step. Infer its type from the previous's step code: \`\`\`typescript\n{prevCode}\n\`\`\` +- \`flow_input.iter.value\` (javascript object): it is ONE ELEMENT of the output of the previous step. {inferTypeGluePrompt} Reply in the following format: input_name: expr` @@ -79,7 +114,7 @@ const gluePrompt = `I'm building a workflow which is a sequence of script steps. My current step code has the following inputs: {inputs}. Determine what to pass as inputs. You can only use the following: - \`flow_input\` (javascript object): general inputs that are passed to the workflow, you can assume any object properties. -- \`prev_output\` (javascript object): previous output is the output of the previous step. Infer its type from the previous's step code: \`\`\`typescript\n{prevCode}\n\`\`\` +- \`prev_output\` (javascript object): previous output is the output of the previous step. {inferTypeGluePrompt} Reply in the following format: input_name: expr` @@ -88,65 +123,83 @@ export async function stepCopilot( module: FlowCopilotModule, deltaCodeStore: Writable, prevCode: string, + prevLang: Script.language | undefined, + isFirstAction: boolean, + isFirstInLoop: boolean, abortController: AbortController ) { - if (module.source === undefined) { - throw new Error('Module not configured') + if (module.source !== 'custom') { + throw new Error('Not a custom module') } - if (module.source === 'hub' && module.selectedCompletion) { - const hubScript = await ScriptService.getHubScriptByPath({ - path: module.selectedCompletion.path - }) - deltaCodeStore.set(hubScript.content) - return hubScript.content - } else { - let prompt = - module.type === 'trigger' - ? triggerPrompt - : prevCode.length > 0 - ? actionPrompt - : firstActionPrompt - prompt = prompt.replace('{description}', module.description).replace('{prevCode}', prevCode) - prompt = await addResourceTypes( + const lang = module.lang ?? 'bun' + const codeLang = lang === 'python3' ? 'python' : 'typescript (Node.js)' + let prompt = + module.type === 'trigger' + ? triggerPrompts[lang] + : isFirstAction + ? firstActionPrompt + : isFirstInLoop + ? loopActionPrompt + : actionPrompt + prompt = prompt + .replace('{codeLang}', codeLang) + .replace( + '{inferTypePrompt}', + prevCode.length > 0 && prevLang + ? (isFirstInLoop ? inferTypeLoopPrompt : inferTypePrompt) + .replace('{prevCode}', prevCode) + .replace('{codeLang}', scriptLangToEditorLang(prevLang)) + : '' + ) + .replace('{additionalInformation}', additionalInfos[lang]) + .replace('{description}', module.description) + prompt = await addResourceTypes( + { + type: 'gen', + language: lang as Script.language, + description: module.description, + dbSchema: undefined + }, + prompt + ) + const code = await deltaCodeCompletion( + [ { - type: 'gen', - language: 'bun' as Script.language, - description: module.description, - dbSchema: undefined + role: 'system', + content: systemPrompt }, - prompt - ) - const code = await deltaCodeCompletion( - [ - { - role: 'system', - content: systemPrompt - }, - { - role: 'user', - content: prompt - } - ], - deltaCodeStore, - abortController - ) - return code - } + { + role: 'user', + content: prompt + } + ], + deltaCodeStore, + abortController + ) + return code } export async function glueCopilot( inputs: string[], prevCode: string, - isLoop: boolean, + prevLang: Script.language | undefined, + isFirstInLoop: boolean, abortController: AbortController ) { let response = await getNonStreamingCompletion( [ { role: 'user', - content: (isLoop ? loopGluePrompt : gluePrompt) + content: (isFirstInLoop ? loopGluePrompt : gluePrompt) .replace('{inputs}', inputs.join(', ')) - .replace('{prevCode}', prevCode) + .replace( + '{inferTypeGluePrompt}', + prevCode.length > 0 && prevLang + ? inferTypeGluePrompt + .replace('{prevCode}', prevCode) + .replace('{codeLang}', scriptLangToEditorLang(prevLang)) + : '' + ) } ], abortController diff --git a/frontend/src/lib/components/flows/map/InsertModuleButton.svelte b/frontend/src/lib/components/flows/map/InsertModuleButton.svelte index 7c04fd4896..0e2d8c1b7a 100644 --- a/frontend/src/lib/components/flows/map/InsertModuleButton.svelte +++ b/frontend/src/lib/components/flows/map/InsertModuleButton.svelte @@ -10,13 +10,27 @@ import { createEventDispatcher } from 'svelte' import Icon from 'svelte-awesome' import { Cross, Repeat, Square } from 'lucide-svelte' + import StepGen from '$lib/components/copilot/StepGen.svelte' + import type { FlowModule } from '$lib/gen' const dispatch = createEventDispatcher() export let trigger = false export let stop = false export let open: boolean | undefined = undefined + export let index: number + export let funcDesc = '' + export let modules: FlowModule[] + + $: !open && (funcDesc = '') + + + + -
    - - {#if trigger} + + {#if funcDesc.length === 0} +
    - {/if} - - - - - - - - - {#if stop} + {#if trigger} + + {/if} + - {/if} -
    + + + + + + + {#if stop} + + {/if} +
    + {/if}
    diff --git a/frontend/src/lib/components/flows/map/MapItem.svelte b/frontend/src/lib/components/flows/map/MapItem.svelte index f789fc7fc0..d071a081ab 100644 --- a/frontend/src/lib/components/flows/map/MapItem.svelte +++ b/frontend/src/lib/components/flows/map/MapItem.svelte @@ -78,6 +78,8 @@ on:new={(e) => { dispatch('insert', { modules, index: idx, detail: e.detail }) }} + index={idx} + {modules} /> {/if} @@ -212,6 +214,8 @@ on:new={(e) => { dispatch('insert', { modules, index: idx + 1, detail: e.detail }) }} + index={idx + 1} + {modules} /> {/if} diff --git a/frontend/src/lib/components/flows/map/VirtualItem.svelte b/frontend/src/lib/components/flows/map/VirtualItem.svelte index b9be4adaa2..4ce4af64f6 100644 --- a/frontend/src/lib/components/flows/map/VirtualItem.svelte +++ b/frontend/src/lib/components/flows/map/VirtualItem.svelte @@ -3,13 +3,13 @@ import type { FlowModule } from '$lib/gen' import { classNames } from '$lib/utils' import { faBolt, faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons' - import { ClipboardCopy, X } from 'lucide-svelte' + import { ClipboardCopy, ExternalLink, X } from 'lucide-svelte' import { createEventDispatcher, getContext } from 'svelte' import { Icon } from 'svelte-awesome' import InsertModuleButton from './InsertModuleButton.svelte' import type { FlowCopilotContext } from '$lib/components/copilot/flow' import { existsOpenaiResourcePath } from '$lib/stores' - import Popup from '$lib/components/common/popup/Popup.svelte' + import Menu from '$lib/components/common/menu/Menu.svelte' export let label: string export let modules: FlowModule[] | undefined @@ -34,6 +34,7 @@ deleteBranch: { module: FlowModule; index: number } }>() let openMenu = false + let openNoCopilot = false const { drawerStore: copilotDrawerStore, currentStepStore: copilotCurrentStepStore } = getContext('FlowCopilotContext') || {} @@ -127,12 +128,53 @@ }) } }} + index={whereInsert == 'after' ? index : index - 1} + modules={modules ?? []} /> {/if} {/if} {#if insertable && modules && label == 'Input'} +
    + + + {#if !$existsOpenaiResourcePath} +
    +

    Enable Windmill AI in the { + close() + }} + >workspace settings +

    +
    + {/if} +
    +
    - -
    - - - - -
    -

    Enable Windmill AI in the { - close(null) - }}>workspace settings.

    -
    -
    -
    {/if} diff --git a/frontend/src/lib/components/flows/previousResults.ts b/frontend/src/lib/components/flows/previousResults.ts index 5903fa37fe..d34fcdb298 100644 --- a/frontend/src/lib/components/flows/previousResults.ts +++ b/frontend/src/lib/components/flows/previousResults.ts @@ -18,7 +18,7 @@ type StepPropPicker = { type ModuleBranches = FlowModule[][] -function dfs(id: string | undefined, flow: Flow, getParents: boolean = true): FlowModule[] { +export function dfs(id: string | undefined, flow: Flow, getParents: boolean = true): FlowModule[] { if (id === undefined) { return [] } @@ -80,22 +80,7 @@ function getFlowInput( } } -export function getStepPropPicker( - flowState: FlowState, - parentModule: FlowModule | undefined, - previousModule: FlowModule | undefined, - id: string, - flow: Flow, - args: any, - include_node: boolean -): StepPropPicker { - const flowInput = getFlowInput( - dfs(parentModule?.id, flow), - flowState, - args, - flow.schema as Schema - ) - +export function getPreviousIds(id: string, flow: Flow, include_node: boolean): string[] { const previousIds = dfs(id, flow, false) .map((x) => { let submodules = getAllSubmodules(x) @@ -112,6 +97,26 @@ export function getStepPropPicker( if (!include_node) { previousIds.shift() } + return previousIds +} + +export function getStepPropPicker( + flowState: FlowState, + parentModule: FlowModule | undefined, + previousModule: FlowModule | undefined, + id: string, + flow: Flow, + args: any, + include_node: boolean +): StepPropPicker { + const flowInput = getFlowInput( + dfs(parentModule?.id, flow), + flowState, + args, + flow.schema as Schema + ) + + const previousIds = getPreviousIds(id, flow, include_node) let priorIds = Object.fromEntries( previousIds.map((id) => [id, flowState[id]?.previewResult ?? {}]).reverse()