Skip to content

Commit 094dae3

Browse files
feat(copilot): add training interface (#1445)
* progress * cleanup UI * progress * fix trigger mode in yaml + copilot side * persist user settings * wrap operations correctly * add trigger mode to add op * remove misplaced comment * add sent notification * remove unused tab:
1 parent 2ee27f9 commit 094dae3

File tree

18 files changed

+8431
-1
lines changed

18 files changed

+8431
-1
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
3+
import { env } from '@/lib/env'
4+
import { createLogger } from '@/lib/logs/console/logger'
5+
6+
const logger = createLogger('CopilotTrainingAPI')
7+
8+
// Schema for the request body
9+
const TrainingDataSchema = z.object({
10+
title: z.string().min(1),
11+
prompt: z.string().min(1),
12+
input: z.any(), // Workflow state (start)
13+
output: z.any(), // Workflow state (end)
14+
operations: z.any(),
15+
})
16+
17+
export async function POST(request: NextRequest) {
18+
try {
19+
// Check for required environment variables
20+
const baseUrl = env.AGENT_INDEXER_URL
21+
if (!baseUrl) {
22+
logger.error('Missing AGENT_INDEXER_URL environment variable')
23+
return NextResponse.json({ error: 'Agent indexer not configured' }, { status: 500 })
24+
}
25+
26+
const apiKey = env.AGENT_INDEXER_API_KEY
27+
if (!apiKey) {
28+
logger.error('Missing AGENT_INDEXER_API_KEY environment variable')
29+
return NextResponse.json(
30+
{ error: 'Agent indexer authentication not configured' },
31+
{ status: 500 }
32+
)
33+
}
34+
35+
// Parse and validate request body
36+
const body = await request.json()
37+
const validationResult = TrainingDataSchema.safeParse(body)
38+
39+
if (!validationResult.success) {
40+
logger.warn('Invalid training data format', { errors: validationResult.error.errors })
41+
return NextResponse.json(
42+
{
43+
error: 'Invalid training data format',
44+
details: validationResult.error.errors,
45+
},
46+
{ status: 400 }
47+
)
48+
}
49+
50+
const { title, prompt, input, output, operations } = validationResult.data
51+
52+
logger.info('Sending training data to agent indexer', {
53+
title,
54+
operationsCount: operations.length,
55+
})
56+
57+
const wrappedOperations = {
58+
operations: operations,
59+
}
60+
61+
// Forward to agent indexer
62+
const upstreamUrl = `${baseUrl}/operations/add`
63+
const upstreamResponse = await fetch(upstreamUrl, {
64+
method: 'POST',
65+
headers: {
66+
'x-api-key': apiKey,
67+
'content-type': 'application/json',
68+
},
69+
body: JSON.stringify({
70+
title,
71+
prompt,
72+
input,
73+
output,
74+
operations: wrappedOperations,
75+
}),
76+
})
77+
78+
const responseData = await upstreamResponse.json()
79+
80+
if (!upstreamResponse.ok) {
81+
logger.error('Agent indexer rejected the data', {
82+
status: upstreamResponse.status,
83+
response: responseData,
84+
})
85+
return NextResponse.json(responseData, { status: upstreamResponse.status })
86+
}
87+
88+
logger.info('Successfully sent training data to agent indexer', {
89+
title,
90+
response: responseData,
91+
})
92+
93+
return NextResponse.json(responseData)
94+
} catch (error) {
95+
logger.error('Failed to send training data to agent indexer', { error })
96+
return NextResponse.json(
97+
{
98+
error: error instanceof Error ? error.message : 'Failed to send training data',
99+
},
100+
{ status: 502 }
101+
)
102+
}
103+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const SettingsSchema = z.object({
2626
})
2727
.optional(),
2828
billingUsageNotificationsEnabled: z.boolean().optional(),
29+
showFloatingControls: z.boolean().optional(),
30+
showTrainingControls: z.boolean().optional(),
2931
})
3032

3133
// Default settings values
@@ -38,6 +40,8 @@ const defaultSettings = {
3840
telemetryEnabled: true,
3941
emailPreferences: {},
4042
billingUsageNotificationsEnabled: true,
43+
showFloatingControls: true,
44+
showTrainingControls: false,
4145
}
4246

4347
export async function GET() {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { getEnv, isTruthy } from '@/lib/env'
5+
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
6+
import { useGeneralStore } from '@/stores/settings/general/store'
7+
import { TrainingFloatingButton } from './training-floating-button'
8+
import { TrainingModal } from './training-modal'
9+
10+
/**
11+
* Main training controls component that manages the training UI
12+
* Only renders if COPILOT_TRAINING_ENABLED env var is set AND user has enabled it in settings
13+
*/
14+
export function TrainingControls() {
15+
const [isEnvEnabled, setIsEnvEnabled] = useState(false)
16+
const showTrainingControls = useGeneralStore((state) => state.showTrainingControls)
17+
const { isTraining, showModal, toggleModal } = useCopilotTrainingStore()
18+
19+
// Check environment variable on mount
20+
useEffect(() => {
21+
// Use getEnv to check if training is enabled
22+
const trainingEnabled = isTruthy(getEnv('NEXT_PUBLIC_COPILOT_TRAINING_ENABLED'))
23+
setIsEnvEnabled(trainingEnabled)
24+
}, [])
25+
26+
// Don't render if not enabled by env var OR user settings
27+
if (!isEnvEnabled || !showTrainingControls) {
28+
return null
29+
}
30+
31+
return (
32+
<>
33+
{/* Floating button to start/stop training */}
34+
<TrainingFloatingButton isTraining={isTraining} onToggleModal={toggleModal} />
35+
36+
{/* Modal for entering prompt and viewing dataset */}
37+
{showModal && <TrainingModal />}
38+
</>
39+
)
40+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use client'
2+
3+
import { Database, Pause } from 'lucide-react'
4+
import { Button } from '@/components/ui/button'
5+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
6+
import { cn } from '@/lib/utils'
7+
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
8+
9+
interface TrainingFloatingButtonProps {
10+
isTraining: boolean
11+
onToggleModal: () => void
12+
}
13+
14+
/**
15+
* Floating button positioned above the diff controls
16+
* Shows training state and allows starting/stopping training
17+
*/
18+
export function TrainingFloatingButton({ isTraining, onToggleModal }: TrainingFloatingButtonProps) {
19+
const { stopTraining } = useCopilotTrainingStore()
20+
21+
const handleClick = () => {
22+
if (isTraining) {
23+
// Stop and save the training session
24+
const dataset = stopTraining()
25+
if (dataset) {
26+
// Show a brief success indicator
27+
const button = document.getElementById('training-button')
28+
if (button) {
29+
button.classList.add('animate-pulse')
30+
setTimeout(() => button.classList.remove('animate-pulse'), 1000)
31+
}
32+
}
33+
} else {
34+
// Open modal to start new training
35+
onToggleModal()
36+
}
37+
}
38+
39+
return (
40+
<div className='-translate-x-1/2 fixed bottom-32 left-1/2 z-30'>
41+
<Tooltip>
42+
<TooltipTrigger asChild>
43+
<Button
44+
id='training-button'
45+
variant='outline'
46+
size='sm'
47+
onClick={handleClick}
48+
className={cn(
49+
'flex items-center gap-2 rounded-[14px] border bg-card/95 px-3 py-2 shadow-lg backdrop-blur-sm transition-all',
50+
'hover:bg-muted/80',
51+
isTraining &&
52+
'border-orange-500 bg-orange-50 dark:border-orange-400 dark:bg-orange-950/30'
53+
)}
54+
>
55+
{isTraining ? (
56+
<>
57+
<Pause className='h-4 w-4 text-orange-600 dark:text-orange-400' />
58+
<span className='font-medium text-orange-700 text-sm dark:text-orange-300'>
59+
Stop Training
60+
</span>
61+
</>
62+
) : (
63+
<>
64+
<Database className='h-4 w-4' />
65+
<span className='font-medium text-sm'>Train Copilot</span>
66+
</>
67+
)}
68+
</Button>
69+
</TooltipTrigger>
70+
<TooltipContent>
71+
{isTraining
72+
? 'Stop recording and save training dataset'
73+
: 'Start recording workflow changes for training'}
74+
</TooltipContent>
75+
</Tooltip>
76+
</div>
77+
)
78+
}

0 commit comments

Comments
 (0)