feat: add search_flows and get_flow_details tools to flow mode AI chat

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-10 12:32:50 +00:00
parent 8e19f9652d
commit 3da0a1f1e8
2 changed files with 285 additions and 11 deletions

157
PLAN.md Normal file
View File

@@ -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<FlowAIChatHelpers>[] = [
// ... 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

View File

@@ -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<FlowAIChatHelpers>[] = [
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<FlowAIChatHelpers>[] = [
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