Get user workflow tool

This commit is contained in:
Siddharth Ganesan
2025-07-08 22:07:48 -07:00
parent f6b25bf727
commit a5883171f9
8 changed files with 445 additions and 281 deletions

View File

@@ -0,0 +1,132 @@
import { NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { workflow as workflowTable } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
const logger = createLogger('GetUserWorkflowAPI')
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { workflowId, includeMetadata = false } = body
if (!workflowId) {
return NextResponse.json(
{ success: false, error: 'Workflow ID is required' },
{ status: 400 }
)
}
logger.info('Fetching workflow for YAML generation', { workflowId })
// Fetch workflow from database
const [workflowRecord] = await db
.select()
.from(workflowTable)
.where(eq(workflowTable.id, workflowId))
.limit(1)
if (!workflowRecord) {
return NextResponse.json(
{ success: false, error: `Workflow ${workflowId} not found` },
{ status: 404 }
)
}
// Try to load from normalized tables first, fallback to JSON blob
let workflowState: any = null
let subBlockValues: Record<string, Record<string, any>> = {}
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
if (normalizedData) {
workflowState = {
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
parallels: normalizedData.parallels,
}
// Extract subblock values from normalized data
Object.entries(normalizedData.blocks).forEach(([blockId, block]) => {
subBlockValues[blockId] = {}
Object.entries((block as any).subBlocks || {}).forEach(([subBlockId, subBlock]) => {
if ((subBlock as any).value !== undefined) {
subBlockValues[blockId][subBlockId] = (subBlock as any).value
}
})
})
} else if (workflowRecord.state) {
// Fallback to JSON blob
workflowState = workflowRecord.state as any
// For JSON blob, subblock values are embedded in the block state
Object.entries((workflowState.blocks as any) || {}).forEach(([blockId, block]) => {
subBlockValues[blockId] = {}
Object.entries(((block as any).subBlocks || {})).forEach(([subBlockId, subBlock]) => {
if ((subBlock as any).value !== undefined) {
subBlockValues[blockId][subBlockId] = (subBlock as any).value
}
})
})
}
if (!workflowState || !workflowState.blocks) {
return NextResponse.json(
{ success: false, error: 'Workflow state is empty or invalid' },
{ status: 400 }
)
}
// Generate YAML using server-side function
const yaml = generateWorkflowYaml(workflowState, subBlockValues)
if (!yaml || yaml.trim() === '') {
return NextResponse.json(
{ success: false, error: 'Generated YAML is empty' },
{ status: 400 }
)
}
// Prepare response
const response: any = {
yaml,
format: 'yaml',
blockCount: Object.keys(workflowState.blocks).length,
edgeCount: (workflowState.edges || []).length,
}
// Add metadata if requested
if (includeMetadata) {
response.metadata = {
workflowId: workflowRecord.id,
name: workflowRecord.name,
description: workflowRecord.description,
workspaceId: workflowRecord.workspaceId,
createdAt: workflowRecord.createdAt,
updatedAt: workflowRecord.updatedAt,
}
}
logger.info('Successfully generated workflow YAML', {
workflowId,
blockCount: response.blockCount,
yamlLength: yaml.length,
})
return NextResponse.json({
success: true,
output: response,
})
} catch (error) {
logger.error('Failed to get workflow YAML:', error)
return NextResponse.json(
{
success: false,
error: `Failed to get workflow YAML: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}
}

View File

@@ -1,56 +0,0 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowYamlStore } from '@/stores/workflows/yaml/store'
const logger = createLogger('GetWorkflowYamlAPI')
export async function POST(request: NextRequest) {
try {
const { includeMetadata = false } = await request.json()
logger.info('Executing get user workflow', { includeMetadata })
// Get the workflow YAML using the same store as the UI
const yamlStore = useWorkflowYamlStore.getState()
const yamlContent = yamlStore.getYaml()
if (!yamlContent) {
return NextResponse.json(
{ success: false, error: 'No workflow content available' },
{ status: 404 }
)
}
let metadata
if (includeMetadata) {
// Get additional workflow metadata if requested
const workflowStore = yamlStore as any // Access internal state
metadata = {
name: workflowStore.workflow?.name || 'Unnamed Workflow',
description: workflowStore.workflow?.description || '',
createdAt: workflowStore.workflow?.createdAt,
updatedAt: workflowStore.workflow?.updatedAt,
}
}
logger.info('Successfully generated workflow YAML', {
includeMetadata,
yamlLength: yamlContent.length,
})
return NextResponse.json({
success: true,
yaml: yamlContent,
metadata: metadata,
})
} catch (error) {
logger.error('Get user workflow API failed', error)
return NextResponse.json(
{
success: false,
error: `Failed to get user workflow: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,44 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator'
const logger = createLogger('WorkflowYamlAPI')
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
logger.info(`[${requestId}] Converting workflow JSON to YAML`)
const body = await request.json()
const { workflowState, subBlockValues, includeMetadata = false } = body
if (!workflowState) {
return NextResponse.json(
{ success: false, error: 'workflowState is required' },
{ status: 400 }
)
}
// Generate YAML using the shared utility
const yamlContent = generateWorkflowYaml(workflowState, subBlockValues)
logger.info(`[${requestId}] Successfully generated YAML`, {
yamlLength: yamlContent.length,
})
return NextResponse.json({
success: true,
yaml: yamlContent,
})
} catch (error) {
logger.error(`[${requestId}] YAML generation failed`, error)
return NextResponse.json(
{
success: false,
error: `Failed to generate YAML: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}
}

View File

@@ -460,6 +460,7 @@ export async function generateChatResponse(
maxTokens: config.chat.maxTokens,
apiKey,
stream,
workflowId: options.workflowId,
})
// Handle StreamingExecution (from providers with tool calls)

View File

@@ -80,43 +80,32 @@ const getUserWorkflowTool: CopilotTool = {
'Get the current user workflow as YAML format. This shows all blocks, their configurations, inputs, and connections in the workflow.',
parameters: {
type: 'object',
properties: {
includeMetadata: {
type: 'boolean',
description: 'Whether to include additional metadata about the workflow (default: false)',
default: false,
},
},
properties: {},
required: [],
},
execute: async (args: Record<string, any>): Promise<CopilotToolResult> => {
try {
const { includeMetadata = false } = args
logger.info('Executing get user workflow', { includeMetadata })
logger.info('Executing get user workflow')
// Import the workflow YAML store dynamically to avoid import issues
const { useWorkflowYamlStore } = await import('@/stores/workflows/yaml/store')
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
// Get the current workflow YAML
// Get the current workflow YAML using the same logic as export
const yamlContent = useWorkflowYamlStore.getState().getYaml()
// Get additional metadata if requested
let metadata = {}
if (includeMetadata) {
const registry = useWorkflowRegistry.getState()
const activeWorkflowId = registry.activeWorkflowId
const activeWorkflow = activeWorkflowId ? registry.workflows[activeWorkflowId] : null
// Get workflow metadata
const registry = useWorkflowRegistry.getState()
const activeWorkflowId = registry.activeWorkflowId
const activeWorkflow = activeWorkflowId ? registry.workflows[activeWorkflowId] : null
if (activeWorkflow) {
metadata = {
workflowId: activeWorkflowId,
name: activeWorkflow.name,
description: activeWorkflow.description,
lastModified: activeWorkflow.lastModified,
workspaceId: activeWorkflow.workspaceId,
}
let metadata = undefined
if (activeWorkflow) {
metadata = {
workflowId: activeWorkflowId,
name: activeWorkflow.name,
description: activeWorkflow.description,
workspaceId: activeWorkflow.workspaceId,
}
}
@@ -126,7 +115,7 @@ const getUserWorkflowTool: CopilotTool = {
success: true,
data: {
yaml: yamlContent,
metadata: includeMetadata ? metadata : undefined,
metadata: metadata,
},
}
} catch (error) {

View File

@@ -0,0 +1,192 @@
import { dump as yamlDump } from 'js-yaml'
import { createLogger } from '@/lib/logs/console-logger'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('WorkflowYamlGenerator')
interface YamlBlock {
type: string
name: string
inputs?: Record<string, any>
preceding?: string[]
following?: string[]
}
interface YamlWorkflow {
version: string
blocks: Record<string, YamlBlock>
}
/**
* Extract input values from a block's subBlocks based on its configuration
* This version works without client-side stores by using the provided subblock values
*/
function extractBlockInputs(
blockState: BlockState,
blockId: string,
subBlockValues?: Record<string, Record<string, any>>
): Record<string, any> {
const blockConfig = getBlock(blockState.type)
const inputs: Record<string, any> = {}
// Get subblock values for this block (if provided)
const blockSubBlockValues = subBlockValues?.[blockId] || {}
if (!blockConfig) {
// For custom blocks like loops/parallels, extract available subBlock values
Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => {
const value = blockSubBlockValues[subBlockId] ?? subBlockState.value
if (value !== undefined && value !== null && value !== '') {
inputs[subBlockId] = value
}
})
return inputs
}
// Process each subBlock configuration
blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => {
const subBlockId = subBlockConfig.id
// Skip hidden or conditional fields that aren't active
if (subBlockConfig.hidden) return
// Get value from provided values or fallback to block state
const value = blockSubBlockValues[subBlockId] ?? blockState.subBlocks[subBlockId]?.value
// Include value if it exists and isn't empty
if (value !== undefined && value !== null && value !== '') {
// Handle different input types appropriately
switch (subBlockConfig.type) {
case 'table':
// Tables are arrays of objects
if (Array.isArray(value) && value.length > 0) {
inputs[subBlockId] = value
}
break
case 'checkbox-list':
// Checkbox lists return arrays
if (Array.isArray(value) && value.length > 0) {
inputs[subBlockId] = value
}
break
case 'code':
// Code blocks should preserve formatting
if (typeof value === 'string' && value.trim()) {
inputs[subBlockId] = value
} else if (typeof value === 'object') {
inputs[subBlockId] = value
}
break
case 'switch':
// Boolean values
inputs[subBlockId] = Boolean(value)
break
case 'slider':
// Numeric values
if (
typeof value === 'number' ||
(typeof value === 'string' && !Number.isNaN(Number(value)))
) {
inputs[subBlockId] = Number(value)
}
break
default:
// Text inputs, dropdowns, etc.
if (typeof value === 'string' && value.trim()) {
inputs[subBlockId] = value.trim()
} else if (
typeof value === 'object' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
inputs[subBlockId] = value
}
break
}
}
})
return inputs
}
/**
* Find preceding blocks for a given block ID
*/
function findPrecedingBlocks(blockId: string, edges: any[]): string[] {
return edges
.filter((edge) => edge.target === blockId)
.map((edge) => edge.source)
.filter((source, index, arr) => arr.indexOf(source) === index) // Remove duplicates
}
/**
* Find following blocks for a given block ID
*/
function findFollowingBlocks(blockId: string, edges: any[]): string[] {
return edges
.filter((edge) => edge.source === blockId)
.map((edge) => edge.target)
.filter((target, index, arr) => arr.indexOf(target) === index) // Remove duplicates
}
/**
* Generate YAML representation of the workflow
* This is the core function extracted from the client store, made server-compatible
*/
export function generateWorkflowYaml(
workflowState: WorkflowState,
subBlockValues?: Record<string, Record<string, any>>
): string {
try {
const yamlWorkflow: YamlWorkflow = {
version: '1.0',
blocks: {},
}
// Process each block
Object.entries(workflowState.blocks).forEach(([blockId, blockState]) => {
const inputs = extractBlockInputs(blockState, blockId, subBlockValues)
const preceding = findPrecedingBlocks(blockId, workflowState.edges)
const following = findFollowingBlocks(blockId, workflowState.edges)
const yamlBlock: YamlBlock = {
type: blockState.type,
name: blockState.name,
}
// Only include inputs if they exist
if (Object.keys(inputs).length > 0) {
yamlBlock.inputs = inputs
}
// Only include connections if they exist
if (preceding.length > 0) {
yamlBlock.preceding = preceding
}
if (following.length > 0) {
yamlBlock.following = following
}
yamlWorkflow.blocks[blockId] = yamlBlock
})
// Convert to YAML with clean formatting
return yamlDump(yamlWorkflow, {
indent: 2,
lineWidth: -1, // Disable line wrapping
noRefs: true,
sortKeys: false,
})
} catch (error) {
logger.error('Failed to generate workflow YAML:', error)
return `# Error generating YAML: ${error instanceof Error ? error.message : 'Unknown error'}`
}
}

View File

@@ -1,28 +1,12 @@
import { dump as yamlDump } from 'js-yaml'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console-logger'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import { generateWorkflowYaml } from '@/lib/workflows/yaml-generator'
import { useSubBlockStore } from '../subblock/store'
import { useWorkflowStore } from '../workflow/store'
import type { BlockState, WorkflowState } from '../workflow/types'
const logger = createLogger('WorkflowYamlStore')
interface YamlBlock {
type: string
name: string
inputs?: Record<string, any>
preceding?: string[]
following?: string[]
}
interface YamlWorkflow {
version: string
blocks: Record<string, YamlBlock>
}
interface WorkflowYamlState {
yaml: string
lastGenerated?: number
@@ -37,165 +21,72 @@ interface WorkflowYamlActions {
type WorkflowYamlStore = WorkflowYamlState & WorkflowYamlActions
/**
* Extract input values from a block's subBlocks based on its configuration
* Get subblock values organized by block for the shared utility
*/
function extractBlockInputs(blockState: BlockState, blockId: string): Record<string, any> {
const blockConfig = getBlock(blockState.type)
function getSubBlockValues() {
const workflowState = useWorkflowStore.getState()
const subBlockStore = useSubBlockStore.getState()
const inputs: Record<string, any> = {}
if (!blockConfig) {
// For custom blocks like loops/parallels, extract available subBlock values
Object.entries(blockState.subBlocks || {}).forEach(([subBlockId, subBlockState]) => {
const value = subBlockStore.getValue(blockId, subBlockId) ?? subBlockState.value
if (value !== undefined && value !== null && value !== '') {
inputs[subBlockId] = value
const subBlockValues: Record<string, Record<string, any>> = {}
Object.entries(workflowState.blocks).forEach(([blockId]) => {
subBlockValues[blockId] = {}
// Get all subblock values for this block
Object.keys(workflowState.blocks[blockId].subBlocks || {}).forEach((subBlockId) => {
const value = subBlockStore.getValue(blockId, subBlockId)
if (value !== undefined) {
subBlockValues[blockId][subBlockId] = value
}
})
return inputs
}
})
return subBlockValues
}
// Process each subBlock configuration
blockConfig.subBlocks.forEach((subBlockConfig: SubBlockConfig) => {
const subBlockId = subBlockConfig.id
// Track if subscriptions have been initialized
let subscriptionsInitialized = false
// Skip hidden or conditional fields that aren't active
if (subBlockConfig.hidden) return
// Initialize subscriptions lazily
function initializeSubscriptions() {
if (subscriptionsInitialized) return
subscriptionsInitialized = true
// Get value from subblock store or fallback to block state
const value =
subBlockStore.getValue(blockId, subBlockId) ?? blockState.subBlocks[subBlockId]?.value
// Auto-refresh YAML when workflow state changes
let lastWorkflowState: { blockCount: number; edgeCount: number } | null = null
// Include value if it exists and isn't empty
if (value !== undefined && value !== null && value !== '') {
// Handle different input types appropriately
switch (subBlockConfig.type) {
case 'table':
// Tables are arrays of objects
if (Array.isArray(value) && value.length > 0) {
inputs[subBlockId] = value
}
break
useWorkflowStore.subscribe((state) => {
const currentState = {
blockCount: Object.keys(state.blocks).length,
edgeCount: state.edges.length,
}
case 'checkbox-list':
// Checkbox lists return arrays
if (Array.isArray(value) && value.length > 0) {
inputs[subBlockId] = value
}
break
// Only refresh if the structure has changed
if (
!lastWorkflowState ||
lastWorkflowState.blockCount !== currentState.blockCount ||
lastWorkflowState.edgeCount !== currentState.edgeCount
) {
lastWorkflowState = currentState
case 'code':
// Code blocks should preserve formatting
if (typeof value === 'string' && value.trim()) {
inputs[subBlockId] = value
} else if (typeof value === 'object') {
inputs[subBlockId] = value
}
break
case 'switch':
// Boolean values
inputs[subBlockId] = Boolean(value)
break
case 'slider':
// Numeric values
if (
typeof value === 'number' ||
(typeof value === 'string' && !Number.isNaN(Number(value)))
) {
inputs[subBlockId] = Number(value)
}
break
default:
// Text inputs, dropdowns, etc.
if (typeof value === 'string' && value.trim()) {
inputs[subBlockId] = value.trim()
} else if (
typeof value === 'object' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
inputs[subBlockId] = value
}
break
}
// Debounce the refresh to avoid excessive updates
const refreshYaml = useWorkflowYamlStore.getState().refreshYaml
setTimeout(refreshYaml, 100)
}
})
return inputs
}
// Subscribe to subblock store changes
let lastSubBlockChangeTime = 0
/**
* Find preceding blocks for a given block ID
*/
function findPrecedingBlocks(blockId: string, edges: any[]): string[] {
return edges
.filter((edge) => edge.target === blockId)
.map((edge) => edge.source)
.filter((source, index, arr) => arr.indexOf(source) === index) // Remove duplicates
}
useSubBlockStore.subscribe((state) => {
const currentTime = Date.now()
/**
* Find following blocks for a given block ID
*/
function findFollowingBlocks(blockId: string, edges: any[]): string[] {
return edges
.filter((edge) => edge.source === blockId)
.map((edge) => edge.target)
.filter((target, index, arr) => arr.indexOf(target) === index) // Remove duplicates
}
// Debounce rapid changes
if (currentTime - lastSubBlockChangeTime > 100) {
lastSubBlockChangeTime = currentTime
/**
* Generate YAML representation of the workflow
*/
function generateWorkflowYaml(workflowState: WorkflowState): string {
try {
const yamlWorkflow: YamlWorkflow = {
version: '1.0',
blocks: {},
const refreshYaml = useWorkflowYamlStore.getState().refreshYaml
setTimeout(refreshYaml, 100)
}
// Process each block
Object.entries(workflowState.blocks).forEach(([blockId, blockState]) => {
const inputs = extractBlockInputs(blockState, blockId)
const preceding = findPrecedingBlocks(blockId, workflowState.edges)
const following = findFollowingBlocks(blockId, workflowState.edges)
const yamlBlock: YamlBlock = {
type: blockState.type,
name: blockState.name,
}
// Only include inputs if they exist
if (Object.keys(inputs).length > 0) {
yamlBlock.inputs = inputs
}
// Only include connections if they exist
if (preceding.length > 0) {
yamlBlock.preceding = preceding
}
if (following.length > 0) {
yamlBlock.following = following
}
yamlWorkflow.blocks[blockId] = yamlBlock
})
// Convert to YAML with clean formatting
return yamlDump(yamlWorkflow, {
indent: 2,
lineWidth: -1, // Disable line wrapping
noRefs: true,
sortKeys: false,
})
} catch (error) {
logger.error('Failed to generate workflow YAML:', error)
return `# Error generating YAML: ${error instanceof Error ? error.message : 'Unknown error'}`
}
})
}
export const useWorkflowYamlStore = create<WorkflowYamlStore>()(
@@ -205,8 +96,12 @@ export const useWorkflowYamlStore = create<WorkflowYamlStore>()(
lastGenerated: undefined,
generateYaml: () => {
// Initialize subscriptions on first use
initializeSubscriptions()
const workflowState = useWorkflowStore.getState()
const yaml = generateWorkflowYaml(workflowState)
const subBlockValues = getSubBlockValues()
const yaml = generateWorkflowYaml(workflowState, subBlockValues)
set({
yaml,
@@ -215,6 +110,9 @@ export const useWorkflowYamlStore = create<WorkflowYamlStore>()(
},
getYaml: () => {
// Initialize subscriptions on first use
initializeSubscriptions()
const currentTime = Date.now()
const { yaml, lastGenerated } = get()
@@ -236,41 +134,3 @@ export const useWorkflowYamlStore = create<WorkflowYamlStore>()(
}
)
)
// Auto-refresh YAML when workflow state changes
let lastWorkflowState: { blockCount: number; edgeCount: number } | null = null
useWorkflowStore.subscribe((state) => {
const currentState = {
blockCount: Object.keys(state.blocks).length,
edgeCount: state.edges.length,
}
// Only refresh if the structure has changed
if (
!lastWorkflowState ||
lastWorkflowState.blockCount !== currentState.blockCount ||
lastWorkflowState.edgeCount !== currentState.edgeCount
) {
lastWorkflowState = currentState
// Debounce the refresh to avoid excessive updates
const refreshYaml = useWorkflowYamlStore.getState().refreshYaml
setTimeout(refreshYaml, 100)
}
})
// Subscribe to subblock store changes
let lastSubBlockChangeTime = 0
useSubBlockStore.subscribe((state) => {
const currentTime = Date.now()
// Debounce rapid changes
if (currentTime - lastSubBlockChangeTime > 100) {
lastSubBlockChangeTime = currentTime
const refreshYaml = useWorkflowYamlStore.getState().refreshYaml
setTimeout(refreshYaml, 100)
}
})

View File

@@ -15,13 +15,15 @@ export const getUserWorkflowTool: ToolConfig = {
},
},
// Use API endpoint to avoid Node.js module import issues in browser
request: {
url: '/api/workflows/current/yaml',
url: '/api/tools/get-user-workflow',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
workflowId: params._context?.workflowId,
includeMetadata: params.includeMetadata || false,
}),
isInternalRoute: true,