From 3da0a1f1e88442e4bc2decde9ecb3e34bb902e98 Mon Sep 17 00:00:00 2001 From: centdix Date: Tue, 10 Feb 2026 12:32:50 +0000 Subject: [PATCH] feat: add search_flows and get_flow_details tools to flow mode AI chat Co-Authored-By: Claude Opus 4.5 --- PLAN.md | 157 ++++++++++++++++++ .../lib/components/copilot/chat/flow/core.ts | 139 ++++++++++++++-- 2 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000000..8876d4641e --- /dev/null +++ b/PLAN.md @@ -0,0 +1,157 @@ +# Plan 4: Flow Mode — Add Flow Search Tool with Auto-Search Behavior + +## Problem + +Flow mode can search for **scripts** (via `search_scripts`) but cannot search for **flows**. When building a new flow, users can't discover existing flows they could reference as sub-flows. The AI should proactively search for existing flows after any user request, just like it already searches for scripts. + +## Goal + +Add a `search_flows` tool to flow mode and instruct the AI to proactively search for existing flows and scripts after any request, enabling discovery and reuse of flow building blocks. + +## Current Architecture + +- **File**: `frontend/src/lib/components/copilot/chat/flow/core.ts` +- **Existing tools**: `search_scripts`, `search_hub_scripts`, `resource_type`, `get_instructions_for_code_generation`, `get_db_schema`, `set_flow_json`, `inspect_inline_script`, `set_module_code`, `test_run_flow`, `test_run_step`, `get_lint_errors` +- **`search_scripts`** uses `WorkspaceScriptsSearch` (uFuzzy over `ScriptService.listScripts()`) +- **No flow search** — the AI cannot find existing flows to reuse as sub-flow modules + +## Implementation + +### 1. Add `search_flows` tool + +```typescript +const searchFlowsSchema = z.object({ + query: z + .string() + .describe('The query to search for, e.g. "process invoices", "send notification", etc.') +}) + +const searchFlowsToolDef = createToolDef( + searchFlowsSchema, + 'search_flows', + 'Search for flows in the workspace. Returns array of {path, summary} objects. Use this to find existing flows that can be referenced as sub-flow modules.' +) +``` + +### 2. Add `WorkspaceFlowsSearch` class (or reuse shared class) + +Either reuse the shared `WorkspaceRunnablesSearch` from Plan 1, or add a dedicated flow search class: + +```typescript +class WorkspaceFlowsSearch { + private uf: uFuzzy + private workspace: string | undefined = undefined + private flows: Flow[] | undefined = undefined + + constructor() { this.uf = new uFuzzy() } + + private async init(workspace: string) { + this.flows = await FlowService.listFlows({ workspace }) + this.workspace = workspace + } + + async search(query: string, workspace: string) { + if (this.flows === undefined || this.workspace !== workspace) { + await this.init(workspace) + } + const flows = this.flows + if (!flows) throw new Error('Failed to load flows') + + const results = this.uf.search( + flows.map((f) => (emptyString(f.summary) ? f.path : f.summary + ' (' + f.path + ')')), + query.trim() + ) + return results[2]?.map((id) => ({ + path: flows[id].path, + summary: flows[id].summary + })) ?? [] + } +} +``` + +### 3. Add `get_flow_details` tool (to inspect found flows) + +```typescript +const getFlowDetailsSchema = z.object({ + path: z.string().describe('The path of the flow to inspect') +}) + +const getFlowDetailsToolDef = createToolDef( + getFlowDetailsSchema, + 'get_flow_details', + 'Get details of a workspace flow including its modules structure, input schema, and module summaries. Use after search_flows to understand a flow before referencing it.' +) +``` + +Implementation calls `FlowService.getFlowByPath()` and returns: +- Flow summary, description +- Input schema +- Module structure (ids, summaries, types, paths for script references) + +### 4. Update system prompt — Proactive Search Instruction + +This is the key difference from script mode. The system prompt should instruct the AI to **always search for existing flows** when creating or modifying a flow: + +Update the "Creating Flows" section in `prepareFlowSystemMessage()`: + +``` +### Creating Flows + +1. **Search for existing flows and scripts first** (unless user explicitly asks to write from scratch): + - First: `search_flows` to find existing flows that could be reused as sub-flow modules + - Then: `search_scripts` to find workspace scripts + - Then: `search_hub_scripts` (only consider highly relevant results) + - Only create raw scripts if no suitable script or flow is found + +2. **When referencing an existing flow as a sub-module:** + - Use `get_flow_details` to understand the flow's input schema + - Use `type: "flow"` with `path` in the module value + - Define `input_transforms` matching the referenced flow's input schema +``` + +### 5. Register tools + +Add to the `flowTools` array: + +```typescript +export const flowTools: Tool[] = [ + // ... existing tools + { + def: searchFlowsToolDef, + fn: async ({ args, workspace, toolId, toolCallbacks }) => { + toolCallbacks.setToolStatus(toolId, { + content: 'Searching for workspace flows related to "' + args.query + '"...' + }) + const parsedArgs = searchFlowsSchema.parse(args) + const flowResults = await workspaceFlowsSearch.search(parsedArgs.query, workspace) + toolCallbacks.setToolStatus(toolId, { + content: 'Found ' + flowResults.length + ' flows in the workspace related to "' + args.query + '"' + }) + return JSON.stringify(flowResults) + } + }, + { + def: getFlowDetailsToolDef, + fn: async ({ args, workspace, toolId, toolCallbacks }) => { + // ... implementation + } + } +] +``` + +## Files to Modify + +| File | Change | +|------|--------| +| `frontend/src/lib/components/copilot/chat/flow/core.ts` | Add `search_flows` and `get_flow_details` tools, update system prompt, add search class, register tools | +| `frontend/src/lib/components/copilot/chat/shared.ts` | Ensure shared flow search utility if extracted | + +## Testing + +1. Open a flow in flow mode +2. Ask "Create a flow that sends a notification after processing an invoice" +3. AI should proactively search for existing flows related to "notification" and "invoice" +4. AI should search for existing scripts too +5. If matches are found, AI should propose reusing them +6. Ask "I have a flow for Stripe webhook handling, can I add it as a sub-flow here?" +7. AI should find the flow, inspect its inputs, and add it as a `type: "flow"` module diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index f57562b1d2..e55cde2583 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -1,5 +1,7 @@ import { ScriptService, + FlowService, + type Flow, type FlowModule, type InputTransform, type RawScript, @@ -208,10 +210,7 @@ function getExpectedFormat(schema: z.ZodType): string | null { let current = schema // Unwrap optional/nullable to get inner type - while ( - (current as any)._def.type === 'optional' || - (current as any)._def.type === 'nullable' - ) { + while ((current as any)._def.type === 'optional' || (current as any)._def.type === 'nullable') { current = (current as any)._def.innerType if (!current || !(current as any)._def) break } @@ -378,6 +377,68 @@ class WorkspaceScriptsSearch { } } +class WorkspaceFlowsSearch { + private uf: uFuzzy + private workspace: string | undefined = undefined + private flows: Flow[] | undefined = undefined + + constructor() { + this.uf = new uFuzzy() + } + + private async init(workspace: string) { + this.flows = await FlowService.listFlows({ workspace }) + this.workspace = workspace + } + + async search(query: string, workspace: string) { + if (this.flows === undefined || this.workspace !== workspace) { + await this.init(workspace) + } + + const flows = this.flows + + if (!flows) { + throw new Error('Failed to load flows') + } + + const results = this.uf.search( + flows.map((f) => (emptyString(f.summary) ? f.path : f.summary + ' (' + f.path + ')')), + query.trim() + ) + return ( + results[2]?.map((id) => ({ + path: flows[id].path, + summary: flows[id].summary + })) ?? [] + ) + } +} + +const searchFlowsSchema = z.object({ + query: z + .string() + .describe('The query to search for, e.g. "process invoices", "send notification", etc.') +}) + +const searchFlowsToolDef = createToolDef( + searchFlowsSchema, + 'search_flows', + 'Search for flows in the workspace. Returns array of {path, summary} objects. Use this to find existing flows that can be referenced as sub-flow modules.' +) + +const getFlowDetailsSchema = z.object({ + path: z.string().describe('The path of the flow to inspect') +}) + +const getFlowDetailsToolDef = createToolDef( + getFlowDetailsSchema, + 'get_flow_details', + 'Get details of a workspace flow including its modules structure, input schema, and module summaries. Use after search_flows to understand a flow before referencing it.' +) + +const workspaceFlowsSearch = new WorkspaceFlowsSearch() + // Will be overridden by setSchema const testRunFlowSchema = z.object({ args: z @@ -464,6 +525,54 @@ export const flowTools: Tool[] = [ return JSON.stringify(scriptResults) } }, + { + def: searchFlowsToolDef, + fn: async ({ args, workspace, toolId, toolCallbacks }) => { + toolCallbacks.setToolStatus(toolId, { + content: 'Searching for workspace flows related to "' + args.query + '"...' + }) + const parsedArgs = searchFlowsSchema.parse(args) + const flowResults = await workspaceFlowsSearch.search(parsedArgs.query, workspace) + toolCallbacks.setToolStatus(toolId, { + content: + 'Found ' + flowResults.length + ' flows in the workspace related to "' + args.query + '"' + }) + return JSON.stringify(flowResults) + } + }, + { + def: getFlowDetailsToolDef, + fn: async ({ args, workspace, toolId, toolCallbacks }) => { + const parsedArgs = getFlowDetailsSchema.parse(args) + toolCallbacks.setToolStatus(toolId, { + content: 'Getting details for flow "' + parsedArgs.path + '"...' + }) + const flow = await FlowService.getFlowByPath({ + workspace, + path: parsedArgs.path + }) + const modules = flow.value?.modules ?? [] + const moduleSummaries = modules.map((m: FlowModule) => ({ + id: m.id, + summary: m.summary, + type: m.value.type, + ...(m.value.type === 'script' ? { path: m.value.path } : {}), + ...(m.value.type === 'flow' ? { path: m.value.path } : {}), + ...(m.value.type === 'rawscript' ? { language: m.value.language } : {}) + })) + const result = { + path: flow.path, + summary: flow.summary, + description: flow.description, + schema: flow.schema, + modules: moduleSummaries + } + toolCallbacks.setToolStatus(toolId, { + content: 'Retrieved details for flow "' + parsedArgs.path + '"' + }) + return JSON.stringify(result) + } + }, { def: resourceTypeToolDef, fn: async ({ args, toolId, workspace, toolCallbacks }) => { @@ -748,8 +857,7 @@ export const flowTools: Tool[] = [ const path = e.path // Try to find module id for better context const moduleIndex = typeof path[0] === 'number' ? path[0] : undefined - const moduleId = - moduleIndex !== undefined ? parsedModules[moduleIndex]?.id : undefined + const moduleId = moduleIndex !== undefined ? parsedModules[moduleIndex]?.id : undefined const fieldPath = path.slice(1).join('.') let message = e.message @@ -852,6 +960,8 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS - **View existing inline script code** → \`inspect_inline_script\` - **Change module code only** → \`set_module_code\` - **Get language-specific coding instructions** → \`get_instructions_for_code_generation\` (call BEFORE writing code) +- **Find workspace flows** → \`search_flows\` (find existing flows to reuse as sub-flow modules) +- **Get flow details** → \`get_flow_details\` (inspect a flow's input schema and modules before referencing it) - **Find workspace scripts** → \`search_scripts\` - **Find Windmill Hub scripts** → \`search_hub_scripts\` @@ -1058,18 +1168,25 @@ Example: Before writing TypeScript/Bun code, call \`get_instructions_for_code_ge ### Creating Flows -1. **Search for existing scripts first** (unless user explicitly asks to write from scratch): - - First: \`search_scripts\` to find workspace scripts +1. **Search for existing flows and scripts first** (unless user explicitly asks to write from scratch): + - First: \`search_flows\` to find existing flows that could be reused as sub-flow modules + - Then: \`search_scripts\` to find workspace scripts - Then: \`search_hub_scripts\` (only consider highly relevant results) - - Only create raw scripts if no suitable script is found + - Only create raw scripts if no suitable flow or script is found -2. **Build the complete flow using \`set_flow_json\`:** +2. **When referencing an existing flow as a sub-module:** + - Use \`get_flow_details\` to understand the flow's input schema + - Use \`type: "flow"\` with \`path\` in the module value + - Define \`input_transforms\` matching the referenced flow's input schema + +3. **Build the complete flow using \`set_flow_json\`:** + - If using existing flow: use \`type: "flow"\` with \`path\` - If using existing script: use \`type: "script"\` with \`path\` - If creating rawscript: use \`type: "rawscript"\` with \`language\` and \`content\` - **First call \`get_instructions_for_code_generation\` to get the correct code format** - Always define \`input_transforms\` to connect parameters to flow inputs or previous step results -3. **After making code changes, ALWAYS use \`get_lint_errors\` to check for issues.** Fix any errors before proceeding with testing. +4. **After making code changes, ALWAYS use \`get_lint_errors\` to check for issues.** Fix any errors before proceeding with testing. ### AI Agent Modules