Basic ss tes

This commit is contained in:
Siddharth Ganesan
2026-01-30 16:24:27 -08:00
parent eedcde0ce1
commit e37c33eb9f
3 changed files with 167 additions and 12 deletions

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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<Tool
.where(or(...workflowConditions))
.orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id))
// Return both names (for backward compatibility) and full workflow info with IDs
const names = workflows
.map((w) => (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) }
}