Skip to content

Commit 1959edd

Browse files
committed
Merge branch 'improvement/copilot-6' of github.com:simstudioai/sim into improvement/copilot-6
2 parents 1f88bd1 + a0bb02f commit 1959edd

File tree

42 files changed

+30988
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+30988
-178
lines changed

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const ChatMessageSchema = z.object({
5151
workflowId: z.string().optional(),
5252
workspaceId: z.string().optional(),
5353
workflowName: z.string().optional(),
54-
model: z.string().optional().default('claude-opus-4-5'),
54+
model: z.string().optional().default('claude-opus-4-6'),
5555
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
5656
prefetch: z.boolean().optional(),
5757
createNewChat: z.boolean().optional().default(false),
@@ -183,6 +183,29 @@ export async function POST(req: NextRequest) {
183183
})
184184
} catch {}
185185

186+
let currentChat: any = null
187+
let conversationHistory: any[] = []
188+
let actualChatId = chatId
189+
const selectedModel = model || 'claude-opus-4-6'
190+
191+
if (chatId || createNewChat) {
192+
const chatResult = await resolveOrCreateChat({
193+
chatId,
194+
userId: authenticatedUserId,
195+
workflowId,
196+
model: selectedModel,
197+
})
198+
currentChat = chatResult.chat
199+
actualChatId = chatResult.chatId || chatId
200+
conversationHistory = Array.isArray(chatResult.conversationHistory)
201+
? chatResult.conversationHistory
202+
: []
203+
204+
if (chatId && !currentChat) {
205+
return createBadRequestResponse('Chat not found')
206+
}
207+
}
208+
186209
let agentContexts: Array<{ type: string; content: string }> = []
187210
if (Array.isArray(normalizedContexts) && normalizedContexts.length > 0) {
188211
try {
@@ -191,7 +214,8 @@ export async function POST(req: NextRequest) {
191214
normalizedContexts as any,
192215
authenticatedUserId,
193216
message,
194-
resolvedWorkspaceId
217+
resolvedWorkspaceId,
218+
actualChatId
195219
)
196220
agentContexts = processed
197221
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
@@ -213,29 +237,6 @@ export async function POST(req: NextRequest) {
213237
}
214238
}
215239

216-
let currentChat: any = null
217-
let conversationHistory: any[] = []
218-
let actualChatId = chatId
219-
const selectedModel = model || 'claude-opus-4-5'
220-
221-
if (chatId || createNewChat) {
222-
const chatResult = await resolveOrCreateChat({
223-
chatId,
224-
userId: authenticatedUserId,
225-
workflowId,
226-
model: selectedModel,
227-
})
228-
currentChat = chatResult.chat
229-
actualChatId = chatResult.chatId || chatId
230-
conversationHistory = Array.isArray(chatResult.conversationHistory)
231-
? chatResult.conversationHistory
232-
: []
233-
234-
if (chatId && !currentChat) {
235-
return createBadRequestResponse('Chat not found')
236-
}
237-
}
238-
239240
const effectiveMode = mode === 'agent' ? 'build' : mode
240241

241242
const userPermission = resolvedWorkspaceId
@@ -316,10 +317,14 @@ export async function POST(req: NextRequest) {
316317
}
317318

318319
if (stream) {
320+
const executionId = crypto.randomUUID()
321+
const runId = crypto.randomUUID()
319322
const sseStream = createSSEStream({
320323
requestPayload,
321324
userId: authenticatedUserId,
322325
streamId: userMessageIdToUse,
326+
executionId,
327+
runId,
323328
chatId: actualChatId,
324329
currentChat,
325330
isNewChat: conversationHistory.length === 0,
@@ -332,6 +337,8 @@ export async function POST(req: NextRequest) {
332337
userId: authenticatedUserId,
333338
workflowId,
334339
chatId: actualChatId,
340+
executionId,
341+
runId,
335342
goRoute: '/api/copilot',
336343
autoExecuteTools: true,
337344
interactive: true,

apps/sim/app/api/copilot/chat/stream/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export async function GET(request: NextRequest) {
6969
success: true,
7070
events: filteredEvents,
7171
status: meta.status,
72+
executionId: meta.executionId,
73+
runId: meta.runId,
7274
})
7375
}
7476

@@ -77,6 +79,7 @@ export async function GET(request: NextRequest) {
7779
const stream = new ReadableStream({
7880
async start(controller) {
7981
let lastEventId = Number.isFinite(fromEventId) ? fromEventId : 0
82+
let latestMeta = meta
8083

8184
const flushEvents = async () => {
8285
const events = await readStreamEvents(streamId, lastEventId)
@@ -93,6 +96,8 @@ export async function GET(request: NextRequest) {
9396
...entry.event,
9497
eventId: entry.eventId,
9598
streamId: entry.streamId,
99+
executionId: latestMeta?.executionId,
100+
runId: latestMeta?.runId,
96101
}
97102
controller.enqueue(encodeEvent(payload))
98103
}
@@ -104,6 +109,7 @@ export async function GET(request: NextRequest) {
104109
while (Date.now() - startTime < MAX_STREAM_MS) {
105110
const currentMeta = await getStreamMeta(streamId)
106111
if (!currentMeta) break
112+
latestMeta = currentMeta
107113

108114
await flushEvents()
109115

apps/sim/app/api/copilot/chats/route.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ import { copilotChats, permissions, workflow, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { and, desc, eq, isNull, or, sql } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6+
import { z } from 'zod'
7+
import { resolveOrCreateChat } from '@/lib/copilot/chat-lifecycle'
68
import {
79
authenticateCopilotRequestSessionOnly,
10+
createBadRequestResponse,
811
createInternalServerErrorResponse,
912
createUnauthorizedResponse,
1013
} from '@/lib/copilot/request-helpers'
14+
import { taskPubSub } from '@/lib/copilot/task-events'
15+
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
16+
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1117

1218
const logger = createLogger('CopilotChatsListAPI')
1319

20+
const CreateWorkflowCopilotChatSchema = z.object({
21+
workspaceId: z.string().min(1),
22+
workflowId: z.string().min(1),
23+
})
24+
25+
const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6'
26+
1427
export async function GET(_request: NextRequest) {
1528
try {
1629
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
@@ -24,6 +37,7 @@ export async function GET(_request: NextRequest) {
2437
title: copilotChats.title,
2538
workflowId: copilotChats.workflowId,
2639
workspaceId: copilotChats.workspaceId,
40+
conversationId: copilotChats.conversationId,
2741
updatedAt: copilotChats.updatedAt,
2842
})
2943
.from(copilotChats)
@@ -68,3 +82,60 @@ export async function GET(_request: NextRequest) {
6882
return createInternalServerErrorResponse('Failed to fetch user chats')
6983
}
7084
}
85+
86+
/**
87+
* POST /api/copilot/chats
88+
* Creates an empty workflow-scoped copilot chat (same lifecycle as {@link resolveOrCreateChat}).
89+
* Matches mothership's POST /api/mothership/chats pattern so the client always selects a real row id.
90+
*/
91+
export async function POST(request: NextRequest) {
92+
try {
93+
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
94+
if (!isAuthenticated || !userId) {
95+
return createUnauthorizedResponse()
96+
}
97+
98+
const body = await request.json()
99+
const { workspaceId, workflowId } = CreateWorkflowCopilotChatSchema.parse(body)
100+
101+
await assertActiveWorkspaceAccess(workspaceId, userId)
102+
103+
const authorization = await authorizeWorkflowByWorkspacePermission({
104+
workflowId,
105+
userId,
106+
action: 'read',
107+
})
108+
if (!authorization.allowed || !authorization.workflow) {
109+
return NextResponse.json(
110+
{ success: false, error: authorization.message ?? 'Forbidden' },
111+
{ status: authorization.status }
112+
)
113+
}
114+
115+
if (authorization.workflow.workspaceId !== workspaceId) {
116+
return createBadRequestResponse('workflow does not belong to this workspace')
117+
}
118+
119+
const result = await resolveOrCreateChat({
120+
userId,
121+
workflowId,
122+
workspaceId,
123+
model: DEFAULT_COPILOT_MODEL,
124+
type: 'copilot',
125+
})
126+
127+
if (!result.chatId) {
128+
return createInternalServerErrorResponse('Failed to create chat')
129+
}
130+
131+
taskPubSub?.publishStatusChanged({ workspaceId, chatId: result.chatId, type: 'created' })
132+
133+
return NextResponse.json({ success: true, id: result.chatId })
134+
} catch (error) {
135+
if (error instanceof z.ZodError) {
136+
return createBadRequestResponse('workspaceId and workflowId are required')
137+
}
138+
logger.error('Error creating workflow copilot chat:', error)
139+
return createInternalServerErrorResponse('Failed to create chat')
140+
}
141+
}

apps/sim/app/api/copilot/confirm/route.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4+
import { completeAsyncToolCall, upsertAsyncToolCall } from '@/lib/copilot/async-runs/repository'
45
import { REDIS_TOOL_CALL_PREFIX, REDIS_TOOL_CALL_TTL_SECONDS } from '@/lib/copilot/constants'
56
import {
67
authenticateCopilotRequestSessionOnly,
@@ -34,10 +35,34 @@ async function updateToolCallStatus(
3435
message?: string,
3536
data?: Record<string, unknown>
3637
): Promise<boolean> {
38+
const durableStatus =
39+
status === 'success'
40+
? 'completed'
41+
: status === 'cancelled'
42+
? 'cancelled'
43+
: status === 'error' || status === 'rejected'
44+
? 'failed'
45+
: 'pending'
46+
await upsertAsyncToolCall({
47+
runId: crypto.randomUUID(),
48+
toolCallId,
49+
toolName: 'client_tool',
50+
args: {},
51+
status: durableStatus,
52+
}).catch(() => {})
53+
if (durableStatus === 'completed' || durableStatus === 'failed' || durableStatus === 'cancelled') {
54+
await completeAsyncToolCall({
55+
toolCallId,
56+
status: durableStatus,
57+
result: data ?? null,
58+
error: status === 'success' ? null : message || status,
59+
}).catch(() => {})
60+
}
61+
3762
const redis = getRedisClient()
3863
if (!redis) {
39-
logger.warn('Redis client not available for tool confirmation')
40-
return false
64+
logger.warn('Redis client not available for tool confirmation; durable DB mirror only')
65+
return true
4166
}
4267

4368
try {

apps/sim/app/api/mcp/copilot/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636

3737
const logger = createLogger('CopilotMcpAPI')
3838
const mcpRateLimiter = new RateLimiter()
39-
const DEFAULT_COPILOT_MODEL = 'claude-opus-4-5'
39+
const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6'
4040

4141
export const dynamic = 'force-dynamic'
4242
export const runtime = 'nodejs'
@@ -630,7 +630,11 @@ async function handleDirectToolCall(
630630
userId: string
631631
): Promise<CallToolResult> {
632632
try {
633-
const execContext = await prepareExecutionContext(userId, (args.workflowId as string) || '')
633+
const execContext = await prepareExecutionContext(
634+
userId,
635+
(args.workflowId as string) || '',
636+
(args.chatId as string) || undefined
637+
)
634638

635639
const toolCall = {
636640
id: randomUUID(),

0 commit comments

Comments
 (0)