From e37c33eb9f9e032802ca60ef63820207944238a8 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 30 Jan 2026 16:24:27 -0800 Subject: [PATCH] Basic ss tes --- apps/sim/app/api/copilot/chat/route.ts | 68 ++++++++++++++++- apps/sim/app/api/v1/copilot/chat/route.ts | 75 ++++++++++++++++++- .../lib/copilot/orchestrator/tool-executor.ts | 36 ++++++++- 3 files changed, 167 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 8c57cddff..03e1841c0 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' +import { copilotChats, permissions, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, desc, eq } from 'drizzle-orm' +import { and, asc, desc, eq, inArray, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -40,7 +40,8 @@ const ChatMessageSchema = z.object({ message: z.string().min(1, 'Message is required'), userMessageId: z.string().optional(), // ID from frontend for the user message chatId: z.string().optional(), - workflowId: z.string().min(1, 'Workflow ID is required'), + workflowId: z.string().optional(), + workflowName: z.string().optional(), model: z.enum(COPILOT_MODEL_IDS).optional().default('claude-4.5-opus'), mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'), prefetch: z.boolean().optional(), @@ -78,6 +79,54 @@ const ChatMessageSchema = z.object({ commands: z.array(z.string()).optional(), }) +async function resolveWorkflowId( + userId: string, + workflowId?: string, + workflowName?: string +): Promise<{ workflowId: string; workflowName?: string } | null> { + // If workflowId provided, use it directly + if (workflowId) { + return { workflowId } + } + + // Get user's accessible workflows + const workspaceIds = await db + .select({ entityId: permissions.entityId }) + .from(permissions) + .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) + + const workspaceIdList = workspaceIds.map((row) => row.entityId) + + const workflowConditions = [eq(workflow.userId, userId)] + if (workspaceIdList.length > 0) { + workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList)) + } + + const workflows = await db + .select() + .from(workflow) + .where(or(...workflowConditions)) + .orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)) + + if (workflows.length === 0) { + return null + } + + // If workflowName provided, find matching workflow + if (workflowName) { + const match = workflows.find( + (w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase() + ) + if (match) { + return { workflowId: match.id, workflowName: match.name || undefined } + } + return null + } + + // Default to first workflow + return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined } +} + /** * POST /api/copilot/chat * Send messages to sim agent and handle chat persistence @@ -100,7 +149,8 @@ export async function POST(req: NextRequest) { message, userMessageId, chatId, - workflowId, + workflowId: providedWorkflowId, + workflowName, model, mode, prefetch, @@ -113,6 +163,16 @@ export async function POST(req: NextRequest) { contexts, commands, } = ChatMessageSchema.parse(body) + + // Resolve workflowId - if not provided, use first workflow or find by name + const resolved = await resolveWorkflowId(authenticatedUserId, providedWorkflowId, workflowName) + if (!resolved) { + return createBadRequestResponse( + 'No workflows found. Create a workflow first or provide a valid workflowId.' + ) + } + const workflowId = resolved.workflowId + // Ensure we have a consistent user message ID for this request const userMessageIdToUse = userMessageId || crypto.randomUUID() try { diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index 8cd1e0104..412ed8052 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -1,4 +1,7 @@ +import { db } from '@sim/db' +import { permissions, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { and, asc, eq, inArray, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { authenticateV1Request } from '@/app/api/v1/auth' @@ -10,17 +13,71 @@ const logger = createLogger('CopilotHeadlessAPI') const RequestSchema = z.object({ message: z.string().min(1, 'message is required'), - workflowId: z.string().min(1, 'workflowId is required'), + workflowId: z.string().optional(), + workflowName: z.string().optional(), chatId: z.string().optional(), - mode: z.enum(['agent', 'ask', 'plan']).optional().default('agent'), + mode: z.enum(['agent', 'ask', 'plan', 'fast']).optional().default('fast'), model: z.string().optional(), autoExecuteTools: z.boolean().optional().default(true), timeout: z.number().optional().default(300000), }) +async function resolveWorkflowId( + userId: string, + workflowId?: string, + workflowName?: string +): Promise<{ workflowId: string; workflowName?: string } | null> { + // If workflowId provided, use it directly + if (workflowId) { + return { workflowId } + } + + // Get user's accessible workflows + const workspaceIds = await db + .select({ entityId: permissions.entityId }) + .from(permissions) + .where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace'))) + + const workspaceIdList = workspaceIds.map((row) => row.entityId) + + const workflowConditions = [eq(workflow.userId, userId)] + if (workspaceIdList.length > 0) { + workflowConditions.push(inArray(workflow.workspaceId, workspaceIdList)) + } + + const workflows = await db + .select() + .from(workflow) + .where(or(...workflowConditions)) + .orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)) + + if (workflows.length === 0) { + return null + } + + // If workflowName provided, find matching workflow + if (workflowName) { + const match = workflows.find( + (w) => String(w.name || '').trim().toLowerCase() === workflowName.toLowerCase() + ) + if (match) { + return { workflowId: match.id, workflowName: match.name || undefined } + } + return null + } + + // Default to first workflow + return { workflowId: workflows[0].id, workflowName: workflows[0].name || undefined } +} + /** * POST /api/v1/copilot/chat * Headless copilot endpoint for server-side orchestration. + * + * workflowId is optional - if not provided: + * - If workflowName is provided, finds that workflow + * - Otherwise uses the user's first workflow as context + * - The copilot can still operate on any workflow using list_user_workflows */ export async function POST(req: NextRequest) { const auth = await authenticateV1Request(req) @@ -34,9 +91,18 @@ export async function POST(req: NextRequest) { const defaults = getCopilotModel('chat') const selectedModel = parsed.model || defaults.model + // Resolve workflow ID + const resolved = await resolveWorkflowId(auth.userId, parsed.workflowId, parsed.workflowName) + if (!resolved) { + return NextResponse.json( + { success: false, error: 'No workflows found. Create a workflow first or provide a valid workflowId.' }, + { status: 400 } + ) + } + const requestPayload = { message: parsed.message, - workflowId: parsed.workflowId, + workflowId: resolved.workflowId, userId: auth.userId, stream: true, streamToolCalls: true, @@ -44,12 +110,13 @@ export async function POST(req: NextRequest) { mode: parsed.mode, messageId: crypto.randomUUID(), version: SIM_AGENT_VERSION, + headless: true, // Enable cross-workflow operations via workflowId params ...(parsed.chatId ? { chatId: parsed.chatId } : {}), } const result = await orchestrateCopilotStream(requestPayload, { userId: auth.userId, - workflowId: parsed.workflowId, + workflowId: resolved.workflowId, chatId: parsed.chatId, autoExecuteTools: parsed.autoExecuteTools, timeout: parsed.timeout, diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor.ts b/apps/sim/lib/copilot/orchestrator/tool-executor.ts index 3d5ec4d69..15638b0a7 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor.ts @@ -302,7 +302,10 @@ async function executeGetUserWorkflow( return { success: false, error: 'workflowId is required' } } - await ensureWorkflowAccess(workflowId, context.userId) + const { workflow: workflowRecord, workspaceId } = await ensureWorkflowAccess( + workflowId, + context.userId + ) const normalized = await loadWorkflowFromNormalizedTables(workflowId) if (!normalized) { @@ -318,7 +321,16 @@ async function executeGetUserWorkflow( const sanitized = sanitizeForCopilot(workflowState) const userWorkflow = JSON.stringify(sanitized, null, 2) - return { success: true, output: { userWorkflow } } + // Return workflow ID so copilot can use it for subsequent tool calls + return { + success: true, + output: { + workflowId, + workflowName: workflowRecord.name || '', + workspaceId, + userWorkflow, + }, + } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } } @@ -371,7 +383,16 @@ async function executeGetWorkflowFromName( const sanitized = sanitizeForCopilot(workflowState) const userWorkflow = JSON.stringify(sanitized, null, 2) - return { success: true, output: { userWorkflow } } + // Return workflow ID and workspaceId so copilot can use them for subsequent tool calls + return { + success: true, + output: { + workflowId: match.id, + workflowName: match.name || '', + workspaceId: match.workspaceId, + userWorkflow, + }, + } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } } @@ -396,11 +417,18 @@ async function executeListUserWorkflows(context: ExecutionContext): Promise (typeof w.name === 'string' ? w.name : null)) .filter((n): n is string => Boolean(n)) - return { success: true, output: { workflow_names: names } } + const workflowList = workflows.map((w) => ({ + workflowId: w.id, + workflowName: w.name || '', + workspaceId: w.workspaceId, + })) + + return { success: true, output: { workflow_names: names, workflows: workflowList } } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } }