Skip to content

Commit 4bc37db

Browse files
icecrasher321Sg312
andauthored
feat(copilot): JSON sanitization logic + operations sequence diff correctness (#1521)
* add state sending capability * progress * add ability to add title and description to workflow state * progress in language * fix * cleanup code * fix type issue * fix subflow deletion case * Workflow console tool * fix lint --------- Co-authored-by: Siddharth Ganesan <[email protected]>
1 parent 1513862 commit 4bc37db

File tree

11 files changed

+1128
-615
lines changed

11 files changed

+1128
-615
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { env } from '@/lib/env'
3+
import { createLogger } from '@/lib/logs/console/logger'
4+
5+
const logger = createLogger('CopilotTrainingExamplesAPI')
6+
7+
export const runtime = 'nodejs'
8+
export const dynamic = 'force-dynamic'
9+
10+
export async function POST(request: NextRequest) {
11+
const baseUrl = env.AGENT_INDEXER_URL
12+
if (!baseUrl) {
13+
logger.error('Missing AGENT_INDEXER_URL environment variable')
14+
return NextResponse.json({ error: 'Missing AGENT_INDEXER_URL env' }, { status: 500 })
15+
}
16+
17+
const apiKey = env.AGENT_INDEXER_API_KEY
18+
if (!apiKey) {
19+
logger.error('Missing AGENT_INDEXER_API_KEY environment variable')
20+
return NextResponse.json({ error: 'Missing AGENT_INDEXER_API_KEY env' }, { status: 500 })
21+
}
22+
23+
try {
24+
const body = await request.json()
25+
26+
logger.info('Sending workflow example to agent indexer', {
27+
hasJsonField: typeof body?.json === 'string',
28+
})
29+
30+
const upstream = await fetch(`${baseUrl}/examples/add`, {
31+
method: 'POST',
32+
headers: {
33+
'Content-Type': 'application/json',
34+
'x-api-key': apiKey,
35+
},
36+
body: JSON.stringify(body),
37+
})
38+
39+
if (!upstream.ok) {
40+
const errorText = await upstream.text()
41+
logger.error('Agent indexer rejected the example', {
42+
status: upstream.status,
43+
error: errorText,
44+
})
45+
return NextResponse.json({ error: errorText }, { status: upstream.status })
46+
}
47+
48+
const data = await upstream.json()
49+
logger.info('Successfully sent workflow example to agent indexer')
50+
51+
return NextResponse.json(data, {
52+
headers: { 'content-type': 'application/json' },
53+
})
54+
} catch (err) {
55+
const errorMessage = err instanceof Error ? err.message : 'Failed to add example'
56+
logger.error('Failed to send workflow example', { error: err })
57+
return NextResponse.json({ error: errorMessage }, { status: 502 })
58+
}
59+
}

apps/sim/app/api/users/me/settings/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export async function GET() {
7676
telemetryEnabled: userSettings.telemetryEnabled,
7777
emailPreferences: userSettings.emailPreferences ?? {},
7878
billingUsageNotificationsEnabled: userSettings.billingUsageNotificationsEnabled ?? true,
79+
showFloatingControls: userSettings.showFloatingControls ?? true,
80+
showTrainingControls: userSettings.showTrainingControls ?? false,
7981
},
8082
},
8183
{ status: 200 }

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-controls/training-modal.tsx

Lines changed: 146 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { Textarea } from '@/components/ui/textarea'
3030
import { cn } from '@/lib/utils'
3131
import { sanitizeForCopilot } from '@/lib/workflows/json-sanitizer'
3232
import { formatEditSequence } from '@/lib/workflows/training/compute-edit-sequence'
33+
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
3334
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
3435

3536
/**
@@ -52,6 +53,8 @@ export function TrainingModal() {
5253
markDatasetSent,
5354
} = useCopilotTrainingStore()
5455

56+
const currentWorkflow = useCurrentWorkflow()
57+
5558
const [localPrompt, setLocalPrompt] = useState(currentPrompt)
5659
const [localTitle, setLocalTitle] = useState(currentTitle)
5760
const [copiedId, setCopiedId] = useState<string | null>(null)
@@ -63,6 +66,11 @@ export function TrainingModal() {
6366
const [sendingSelected, setSendingSelected] = useState(false)
6467
const [sentDatasets, setSentDatasets] = useState<Set<string>>(new Set())
6568
const [failedDatasets, setFailedDatasets] = useState<Set<string>>(new Set())
69+
const [sendingLiveWorkflow, setSendingLiveWorkflow] = useState(false)
70+
const [liveWorkflowSent, setLiveWorkflowSent] = useState(false)
71+
const [liveWorkflowFailed, setLiveWorkflowFailed] = useState(false)
72+
const [liveWorkflowTitle, setLiveWorkflowTitle] = useState('')
73+
const [liveWorkflowDescription, setLiveWorkflowDescription] = useState('')
6674

6775
const handleStart = () => {
6876
if (localTitle.trim() && localPrompt.trim()) {
@@ -285,6 +293,46 @@ export function TrainingModal() {
285293
}
286294
}
287295

296+
const handleSendLiveWorkflow = async () => {
297+
if (!liveWorkflowTitle.trim() || !liveWorkflowDescription.trim()) {
298+
return
299+
}
300+
301+
setLiveWorkflowSent(false)
302+
setLiveWorkflowFailed(false)
303+
setSendingLiveWorkflow(true)
304+
305+
try {
306+
const sanitizedWorkflow = sanitizeForCopilot(currentWorkflow.workflowState)
307+
308+
const response = await fetch('/api/copilot/training/examples', {
309+
method: 'POST',
310+
headers: { 'Content-Type': 'application/json' },
311+
body: JSON.stringify({
312+
json: JSON.stringify(sanitizedWorkflow),
313+
source_path: liveWorkflowTitle,
314+
summary: liveWorkflowDescription,
315+
}),
316+
})
317+
318+
if (!response.ok) {
319+
const error = await response.json()
320+
throw new Error(error.error || 'Failed to send live workflow')
321+
}
322+
323+
setLiveWorkflowSent(true)
324+
setLiveWorkflowTitle('')
325+
setLiveWorkflowDescription('')
326+
setTimeout(() => setLiveWorkflowSent(false), 5000)
327+
} catch (error) {
328+
console.error('Failed to send live workflow:', error)
329+
setLiveWorkflowFailed(true)
330+
setTimeout(() => setLiveWorkflowFailed(false), 5000)
331+
} finally {
332+
setSendingLiveWorkflow(false)
333+
}
334+
}
335+
288336
return (
289337
<Dialog open={showModal} onOpenChange={toggleModal}>
290338
<DialogContent className='max-w-3xl'>
@@ -335,24 +383,24 @@ export function TrainingModal() {
335383
)}
336384

337385
<Tabs defaultValue={isTraining ? 'datasets' : 'new'} className='mt-4'>
338-
<TabsList className='grid w-full grid-cols-2'>
386+
<TabsList className='grid w-full grid-cols-3'>
339387
<TabsTrigger value='new' disabled={isTraining}>
340388
New Session
341389
</TabsTrigger>
342390
<TabsTrigger value='datasets'>Datasets ({datasets.length})</TabsTrigger>
391+
<TabsTrigger value='live'>Send Live State</TabsTrigger>
343392
</TabsList>
344393

345394
{/* New Training Session Tab */}
346395
<TabsContent value='new' className='space-y-4'>
347-
{startSnapshot && (
348-
<div className='rounded-lg border bg-muted/50 p-3'>
349-
<p className='font-medium text-muted-foreground text-sm'>Current Workflow State</p>
350-
<p className='text-sm'>
351-
{Object.keys(startSnapshot.blocks).length} blocks, {startSnapshot.edges.length}{' '}
352-
edges
353-
</p>
354-
</div>
355-
)}
396+
<div className='rounded-lg border bg-muted/50 p-3'>
397+
<p className='mb-2 font-medium text-muted-foreground text-sm'>
398+
Current Workflow State
399+
</p>
400+
<p className='text-sm'>
401+
{currentWorkflow.getBlockCount()} blocks, {currentWorkflow.getEdgeCount()} edges
402+
</p>
403+
</div>
356404

357405
<div className='space-y-2'>
358406
<Label htmlFor='title'>Title</Label>
@@ -628,6 +676,94 @@ export function TrainingModal() {
628676
</>
629677
)}
630678
</TabsContent>
679+
680+
{/* Send Live State Tab */}
681+
<TabsContent value='live' className='space-y-4'>
682+
<div className='rounded-lg border bg-muted/50 p-3'>
683+
<p className='mb-2 font-medium text-muted-foreground text-sm'>
684+
Current Workflow State
685+
</p>
686+
<p className='text-sm'>
687+
{currentWorkflow.getBlockCount()} blocks, {currentWorkflow.getEdgeCount()} edges
688+
</p>
689+
</div>
690+
691+
<div className='space-y-2'>
692+
<Label htmlFor='live-title'>Title</Label>
693+
<Input
694+
id='live-title'
695+
placeholder='e.g., Customer Onboarding Workflow'
696+
value={liveWorkflowTitle}
697+
onChange={(e) => setLiveWorkflowTitle(e.target.value)}
698+
/>
699+
<p className='text-muted-foreground text-xs'>
700+
A short title identifying this workflow
701+
</p>
702+
</div>
703+
704+
<div className='space-y-2'>
705+
<Label htmlFor='live-description'>Description</Label>
706+
<Textarea
707+
id='live-description'
708+
placeholder='Describe what this workflow does...'
709+
value={liveWorkflowDescription}
710+
onChange={(e) => setLiveWorkflowDescription(e.target.value)}
711+
rows={3}
712+
/>
713+
<p className='text-muted-foreground text-xs'>
714+
Explain the purpose and functionality of this workflow
715+
</p>
716+
</div>
717+
718+
<Button
719+
onClick={handleSendLiveWorkflow}
720+
disabled={
721+
!liveWorkflowTitle.trim() ||
722+
!liveWorkflowDescription.trim() ||
723+
sendingLiveWorkflow ||
724+
currentWorkflow.getBlockCount() === 0
725+
}
726+
className='w-full'
727+
>
728+
{sendingLiveWorkflow ? (
729+
<>
730+
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent' />
731+
Sending...
732+
</>
733+
) : liveWorkflowSent ? (
734+
<>
735+
<CheckCircle2 className='mr-2 h-4 w-4' />
736+
Sent Successfully
737+
</>
738+
) : liveWorkflowFailed ? (
739+
<>
740+
<XCircle className='mr-2 h-4 w-4' />
741+
Failed - Try Again
742+
</>
743+
) : (
744+
<>
745+
<Send className='mr-2 h-4 w-4' />
746+
Send Live Workflow State
747+
</>
748+
)}
749+
</Button>
750+
751+
{liveWorkflowSent && (
752+
<div className='rounded-lg border bg-green-50 p-3 dark:bg-green-950/30'>
753+
<p className='text-green-700 text-sm dark:text-green-300'>
754+
Workflow state sent successfully!
755+
</p>
756+
</div>
757+
)}
758+
759+
{liveWorkflowFailed && (
760+
<div className='rounded-lg border bg-red-50 p-3 dark:bg-red-950/30'>
761+
<p className='text-red-700 text-sm dark:text-red-300'>
762+
Failed to send workflow state. Please try again.
763+
</p>
764+
</div>
765+
)}
766+
</TabsContent>
631767
</Tabs>
632768
</DialogContent>
633769
</Dialog>

apps/sim/lib/copilot/process-contents.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,6 @@ async function processWorkflowFromDb(
268268
logger.info('Processed sanitized workflow context', {
269269
workflowId,
270270
blocks: Object.keys(sanitizedState.blocks || {}).length,
271-
edges: sanitizedState.edges.length,
272271
})
273272
// Use the provided kind for the type
274273
return { type: kind, tag, content }

apps/sim/lib/copilot/registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ const ExecutionEntry = z.object({
262262
totalTokens: z.number().nullable(),
263263
blockExecutions: z.array(z.any()), // can be detailed per need
264264
output: z.any().optional(),
265+
errorMessage: z.string().optional(),
266+
errorBlock: z
267+
.object({
268+
blockId: z.string().optional(),
269+
blockName: z.string().optional(),
270+
blockType: z.string().optional(),
271+
})
272+
.optional(),
265273
})
266274

267275
export const ToolResultSchemas = {

apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,35 @@ export class EditWorkflowClientTool extends BaseClientTool {
9898

9999
// Prepare currentUserWorkflow JSON from stores to preserve block IDs
100100
let currentUserWorkflow = args?.currentUserWorkflow
101-
if (!currentUserWorkflow) {
101+
const diffStoreState = useWorkflowDiffStore.getState()
102+
let usedDiffWorkflow = false
103+
104+
if (!currentUserWorkflow && diffStoreState.isDiffReady && diffStoreState.diffWorkflow) {
105+
try {
106+
const diffWorkflow = diffStoreState.diffWorkflow
107+
const normalizedDiffWorkflow = {
108+
...diffWorkflow,
109+
blocks: diffWorkflow.blocks || {},
110+
edges: diffWorkflow.edges || [],
111+
loops: diffWorkflow.loops || {},
112+
parallels: diffWorkflow.parallels || {},
113+
}
114+
currentUserWorkflow = JSON.stringify(normalizedDiffWorkflow)
115+
usedDiffWorkflow = true
116+
logger.info('Using diff workflow state as base for edit_workflow operations', {
117+
toolCallId: this.toolCallId,
118+
blocksCount: Object.keys(normalizedDiffWorkflow.blocks).length,
119+
edgesCount: normalizedDiffWorkflow.edges.length,
120+
})
121+
} catch (e) {
122+
logger.warn(
123+
'Failed to serialize diff workflow state; falling back to active workflow',
124+
e as any
125+
)
126+
}
127+
}
128+
129+
if (!currentUserWorkflow && !usedDiffWorkflow) {
102130
try {
103131
const workflowStore = useWorkflowStore.getState()
104132
const fullState = workflowStore.getWorkflowState()

0 commit comments

Comments
 (0)