Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions apps/sim/app/api/copilot/feedback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createInternalServerErrorResponse,
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
import { copilotFeedback } from '@/db/schema'

const logger = createLogger('CopilotFeedbackAPI')

// Schema for feedback submission
const FeedbackSchema = z.object({
userQuery: z.string().min(1, 'User query is required'),
agentResponse: z.string().min(1, 'Agent response is required'),
isPositiveFeedback: z.boolean(),
feedback: z.string().optional(),
workflowYaml: z.string().optional(), // Optional workflow YAML when edit/build workflow tools were used
})

/**
* POST /api/copilot/feedback
* Submit feedback for a copilot interaction
*/
export async function POST(req: NextRequest) {
const tracker = createRequestTracker()

try {
// Authenticate user using the same pattern as other copilot routes
const { userId: authenticatedUserId, isAuthenticated } =
await authenticateCopilotRequestSessionOnly()

if (!isAuthenticated || !authenticatedUserId) {
return createUnauthorizedResponse()
}

const body = await req.json()
const { userQuery, agentResponse, isPositiveFeedback, feedback, workflowYaml } =
FeedbackSchema.parse(body)

logger.info(`[${tracker.requestId}] Processing copilot feedback submission`, {
userId: authenticatedUserId,
isPositiveFeedback,
userQueryLength: userQuery.length,
agentResponseLength: agentResponse.length,
hasFeedback: !!feedback,
hasWorkflowYaml: !!workflowYaml,
workflowYamlLength: workflowYaml?.length || 0,
})

// Insert feedback into the database
const [feedbackRecord] = await db
.insert(copilotFeedback)
.values({
userQuery,
agentResponse,
isPositive: isPositiveFeedback,
feedback: feedback || null,
workflowYaml: workflowYaml || null,
})
.returning()

logger.info(`[${tracker.requestId}] Successfully saved copilot feedback`, {
feedbackId: feedbackRecord.feedbackId,
userId: authenticatedUserId,
isPositive: isPositiveFeedback,
duration: tracker.getDuration(),
})

return NextResponse.json({
success: true,
feedbackId: feedbackRecord.feedbackId,
message: 'Feedback submitted successfully',
metadata: {
requestId: tracker.requestId,
duration: tracker.getDuration(),
},
})
} catch (error) {
const duration = tracker.getDuration()

if (error instanceof z.ZodError) {
logger.error(`[${tracker.requestId}] Validation error:`, {
duration,
errors: error.errors,
})
return createBadRequestResponse(
`Invalid request data: ${error.errors.map((e) => e.message).join(', ')}`
)
}

logger.error(`[${tracker.requestId}] Error submitting copilot feedback:`, {
duration,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
})

return createInternalServerErrorResponse('Failed to submit feedback')
}
}

/**
* GET /api/copilot/feedback
* Get all feedback records (for analytics)
*/
export async function GET(req: NextRequest) {
const tracker = createRequestTracker()

try {
// Authenticate user
const { userId: authenticatedUserId, isAuthenticated } =
await authenticateCopilotRequestSessionOnly()

if (!isAuthenticated || !authenticatedUserId) {
return createUnauthorizedResponse()
}

// Get all feedback records
const feedbackRecords = await db
.select({
feedbackId: copilotFeedback.feedbackId,
userQuery: copilotFeedback.userQuery,
agentResponse: copilotFeedback.agentResponse,
isPositive: copilotFeedback.isPositive,
feedback: copilotFeedback.feedback,
workflowYaml: copilotFeedback.workflowYaml,
createdAt: copilotFeedback.createdAt,
})
.from(copilotFeedback)

logger.info(`[${tracker.requestId}] Retrieved ${feedbackRecords.length} feedback records`)

return NextResponse.json({
success: true,
feedback: feedbackRecords,
metadata: {
requestId: tracker.requestId,
duration: tracker.getDuration(),
},
})
} catch (error) {
logger.error(`[${tracker.requestId}] Error retrieving copilot feedback:`, error)
return createInternalServerErrorResponse('Failed to retrieve feedback')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { InlineToolCall } from '@/lib/copilot/tools/inline-tool-call'
import { usePreviewStore } from '@/stores/copilot/preview-store'
import { useCopilotStore } from '@/stores/copilot/store'
import type { CopilotMessage as CopilotMessageType } from '@/stores/copilot/types'

Expand Down Expand Up @@ -214,8 +215,17 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
messageCheckpoints: allMessageCheckpoints,
revertToCheckpoint,
isRevertingCheckpoint,
currentChat,
messages,
workflowId,
} = useCopilotStore()

// Get preview store for accessing workflow YAML after rejection
const { getPreviewByToolCall, getLatestPendingPreview } = usePreviewStore()

// Import COPILOT_TOOL_IDS - placing it here since it's needed in multiple functions
const WORKFLOW_TOOL_NAMES = ['build_workflow', 'edit_workflow']

// Get checkpoints for this message if it's a user message
const messageCheckpoints = isUser ? allMessageCheckpoints[message.id] || [] : []
const hasCheckpoints = messageCheckpoints.length > 0
Expand All @@ -226,16 +236,180 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
setShowCopySuccess(true)
}

const handleUpvote = () => {
// Helper function to get the full assistant response content
const getFullAssistantContent = (message: CopilotMessageType) => {
// First try the direct content
if (message.content?.trim()) {
return message.content
}

// If no direct content, build from content blocks
if (message.contentBlocks && message.contentBlocks.length > 0) {
return message.contentBlocks
.filter((block) => block.type === 'text')
.map((block) => block.content)
.join('')
}

return message.content || ''
}

// Helper function to find the last user query before this assistant message
const getLastUserQuery = () => {
const messageIndex = messages.findIndex((msg) => msg.id === message.id)
if (messageIndex === -1) return null

// Look backwards from this message to find the last user message
for (let i = messageIndex - 1; i >= 0; i--) {
if (messages[i].role === 'user') {
return messages[i].content
}
}
return null
}

// Helper function to extract workflow YAML from workflow tool calls
const getWorkflowYaml = () => {
// Step 1: Check both toolCalls array and contentBlocks for workflow tools
const allToolCalls = [
...(message.toolCalls || []),
...(message.contentBlocks || [])
.filter((block) => block.type === 'tool_call')
.map((block) => (block as any).toolCall),
]

// Find workflow tools (build_workflow or edit_workflow)
const workflowTools = allToolCalls.filter((toolCall) =>
WORKFLOW_TOOL_NAMES.includes(toolCall?.name)
)

// Extract YAML content from workflow tools in the current message
for (const toolCall of workflowTools) {
// Try various locations where YAML content might be stored
const yamlContent =
toolCall.result?.yamlContent ||
toolCall.result?.data?.yamlContent ||
toolCall.input?.yamlContent ||
toolCall.input?.data?.yamlContent

if (yamlContent && typeof yamlContent === 'string' && yamlContent.trim()) {
console.log('Found workflow YAML in tool call:', {
toolCallId: toolCall.id,
toolName: toolCall.name,
yamlLength: yamlContent.length,
})
return yamlContent
}
}

// Step 2: Check copilot store's preview YAML (set when workflow tools execute)
if (currentChat?.previewYaml?.trim()) {
console.log('Found workflow YAML in copilot store preview:', {
yamlLength: currentChat.previewYaml.length,
})
return currentChat.previewYaml
}

// Step 3: Check preview store for recent workflow tool calls from this message
for (const toolCall of workflowTools) {
if (toolCall.id) {
const preview = getPreviewByToolCall(toolCall.id)
if (preview?.yamlContent?.trim()) {
console.log('Found workflow YAML in preview store:', {
toolCallId: toolCall.id,
previewId: preview.id,
yamlLength: preview.yamlContent.length,
})
return preview.yamlContent
}
}
}

// Step 4: If this message contains workflow tools but no YAML found yet,
// try to get the latest pending preview for this workflow (fallback)
if (workflowTools.length > 0 && workflowId) {
const latestPreview = getLatestPendingPreview(workflowId, currentChat?.id)
if (latestPreview?.yamlContent?.trim()) {
console.log('Found workflow YAML in latest pending preview:', {
previewId: latestPreview.id,
yamlLength: latestPreview.yamlContent.length,
})
return latestPreview.yamlContent
}
}

return null
}

// Function to submit feedback
const submitFeedback = async (isPositive: boolean) => {
const userQuery = getLastUserQuery()
if (!userQuery) {
console.error('No user query found for feedback submission')
return
}

const agentResponse = getFullAssistantContent(message)
if (!agentResponse.trim()) {
console.error('No agent response content available for feedback submission')
return
}

// Get workflow YAML if this message contains workflow tools
const workflowYaml = getWorkflowYaml()

try {
const requestBody: any = {
userQuery,
agentResponse,
isPositiveFeedback: isPositive,
}

// Only include workflowYaml if it exists
if (workflowYaml) {
requestBody.workflowYaml = workflowYaml
console.log('Including workflow YAML in feedback:', {
yamlLength: workflowYaml.length,
yamlPreview: workflowYaml.substring(0, 100),
})
}

const response = await fetch('/api/copilot/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})

if (!response.ok) {
throw new Error(`Failed to submit feedback: ${response.statusText}`)
}

const result = await response.json()
console.log('Feedback submitted successfully:', result)
} catch (error) {
console.error('Error submitting feedback:', error)
// Could show a toast or error message to user here
}
}

const handleUpvote = async () => {
// Reset downvote if it was active
setShowDownvoteSuccess(false)
setShowUpvoteSuccess(true)

// Submit positive feedback
await submitFeedback(true)
}

const handleDownvote = () => {
const handleDownvote = async () => {
// Reset upvote if it was active
setShowUpvoteSuccess(false)
setShowDownvoteSuccess(true)

// Submit negative feedback
await submitFeedback(false)
}

const handleRevertToCheckpoint = () => {
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/db/migrations/0066_petite_wildside.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE TABLE "copilot_feedback" (
"feedback_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_query" text NOT NULL,
"agent_response" text NOT NULL,
"is_positive" boolean NOT NULL,
"feedback" text,
"workflow_yaml" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "user_stats" ALTER COLUMN "current_usage_limit" SET DEFAULT '10';--> statement-breakpoint
CREATE INDEX "copilot_feedback_is_positive_idx" ON "copilot_feedback" USING btree ("is_positive");--> statement-breakpoint
CREATE INDEX "copilot_feedback_created_at_idx" ON "copilot_feedback" USING btree ("created_at");
Loading