/** * Feedback AI Agent * * Automatically analyzes feedback submissions and generates: * - Summary of the feedback * - Impact analysis (affected areas, users, systems) * - Suggested action items * - Estimated effort (low/medium/high) * - Priority recommendations * * Triggers email notifications to the reporter and assigned team. */ import { v } from "convex/values"; import { Agent, createThread } from "@convex-dev/agent"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { components, internal } from "../_generated/api"; import { action, internalAction, internalQuery } from "../_generated/server"; import type { FeedbackPriority, Effort } from "../schema"; // Helper to create OpenRouter provider dynamically // This is created dynamically because Convex components don't inherit parent env vars function createOpenRouterProvider(apiKey: string) { return createOpenAICompatible({ name: "openrouter", baseURL: "https://openrouter.ai/api/v1", headers: { Authorization: `Bearer ${apiKey}`, }, }); } // Agent name constant const AGENT_NAME = "Feedback Analyst"; // Agent instructions (shared between dynamic agent instances) const FEEDBACK_AGENT_INSTRUCTIONS = `You are the Feedback Analyst, an AI assistant that analyzes user feedback submissions. When given feedback, you will: 1. **Summarize** the feedback in 2-3 clear sentences 2. **Analyze Impact** - identify what areas, users, or systems are affected 3. **Suggest Action Items** - provide 3-5 concrete next steps to address the feedback 4. **Estimate Effort** - rate as "low" (< 1 week), "medium" (1-4 weeks), or "high" (> 4 weeks) 5. **Recommend Priority** - suggest "nice_to_have", "important", or "critical" based on the feedback content ## Response Format Always respond in the following JSON format: \`\`\`json { "summary": "A 2-3 sentence summary of the feedback", "impactAnalysis": "Detailed analysis of what's affected and potential consequences", "actionItems": ["Action item 1", "Action item 2", "Action item 3"], "estimatedEffort": "low" | "medium" | "high", "suggestedPriority": "nice_to_have" | "important" | "critical", "reasoning": "Brief explanation for your priority and effort estimates" } \`\`\` ## Guidelines - Be objective and constructive in your analysis - Consider both technical and business impact - Action items should be specific and actionable - Effort estimation should account for development, testing, and deployment - Priority should reflect urgency and user impact`; // ============================================================================ // Internal Queries // ============================================================================ /** * Get feedback by ID for analysis */ export const getFeedbackForAnalysis = internalQuery({ args: { feedbackId: v.id("feedback"), }, returns: v.union( v.object({ _id: v.id("feedback"), type: v.string(), title: v.string(), description: v.string(), priority: v.string(), reporterName: v.string(), reporterEmail: v.string(), url: v.string(), browserInfo: v.string(), }), v.null() ), handler: async (ctx, args) => { const feedback = await ctx.db.get(args.feedbackId); if (!feedback) { return null; } return { _id: feedback._id, type: feedback.type, title: feedback.title, description: feedback.description, priority: feedback.priority, reporterName: feedback.reporterName, reporterEmail: feedback.reporterEmail, url: feedback.url, browserInfo: feedback.browserInfo, }; }, }); // ============================================================================ // Main Processing Action // ============================================================================ /** * Analyze feedback using AI and send notifications. * This is the main entry point triggered after feedback creation. */ export const processFeedback = internalAction({ args: { feedbackId: v.id("feedback"), // Optional API keys - passed from parent app since components don't inherit env vars openRouterApiKey: v.optional(v.string()), resendApiKey: v.optional(v.string()), resendFromEmail: v.optional(v.string()), }, returns: v.object({ success: v.boolean(), error: v.optional(v.string()), }), handler: async (ctx, args): Promise<{ success: boolean; error?: string }> => { // Check if OpenRouter API key is configured - prefer args (from parent app), fallback to env const apiKey = args.openRouterApiKey || process.env.OPENROUTER_API_KEY; if (!apiKey) { console.log("OPENROUTER_API_KEY not configured, skipping AI analysis"); // Still try to send notifications without AI analysis try { await ctx.runAction(internal.emails.feedbackEmails.sendFeedbackNotifications, { feedbackId: args.feedbackId, analysis: null, resendApiKey: args.resendApiKey, resendFromEmail: args.resendFromEmail, }); } catch { console.log("Email notifications not configured"); } return { success: true }; } // Get the feedback const feedback = await ctx.runQuery(internal.agents.feedbackAgent.getFeedbackForAnalysis, { feedbackId: args.feedbackId, }); if (!feedback) { return { success: false, error: "Feedback not found" }; } // Create a thread for this analysis const threadId = await createThread(ctx, components.agent, { title: `Feedback Analysis: ${feedback.title}`, }); // Create OpenRouter provider dynamically with the API key const openrouter = createOpenRouterProvider(apiKey); // Create agent dynamically to ensure API key is available at runtime const feedbackAgent = new Agent(components.agent, { name: AGENT_NAME, languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"), instructions: FEEDBACK_AGENT_INSTRUCTIONS, tools: {}, maxSteps: 3, }); // Build the prompt for analysis const analysisPrompt = `Please analyze the following feedback submission: **Type:** ${feedback.type.replace("_", " ")} **Title:** ${feedback.title} **Description:** ${feedback.description} **Reporter Priority:** ${feedback.priority.replace("_", " ")} **Submitted From:** ${feedback.url} Provide your analysis in the JSON format specified.`; // Generate analysis using the agent let result: { text: string }; try { result = await feedbackAgent.generateText( ctx, { threadId }, { prompt: analysisPrompt } ); } catch (error) { console.error("AI analysis failed:", error); return { success: false, error: `AI analysis failed: ${error instanceof Error ? error.message : String(error)}` }; } // Parse the AI response let analysis: { summary: string; impactAnalysis: string; actionItems: string[]; estimatedEffort: Effort; suggestedPriority: FeedbackPriority; }; try { // Extract JSON from the response (handle markdown code blocks) const jsonMatch = result.text.match(/```json\s*([\s\S]*?)\s*```/); const jsonStr = jsonMatch ? jsonMatch[1] : result.text; const parsed = JSON.parse(jsonStr); analysis = { summary: parsed.summary || "Unable to generate summary", impactAnalysis: parsed.impactAnalysis || "Unable to generate impact analysis", actionItems: Array.isArray(parsed.actionItems) ? parsed.actionItems : [], estimatedEffort: ["low", "medium", "high"].includes(parsed.estimatedEffort) ? parsed.estimatedEffort : "medium", suggestedPriority: ["nice_to_have", "important", "critical"].includes(parsed.suggestedPriority) ? parsed.suggestedPriority : feedback.priority as FeedbackPriority, }; } catch { // If parsing fails, create a basic analysis from the text analysis = { summary: result.text.substring(0, 500), impactAnalysis: "AI analysis parsing failed. Please review manually.", actionItems: ["Review feedback manually", "Contact reporter for clarification"], estimatedEffort: "medium", suggestedPriority: feedback.priority as FeedbackPriority, }; } // Update feedback with analysis await ctx.runMutation(internal.feedback.updateWithAnalysis, { feedbackId: args.feedbackId, aiSummary: analysis.summary, aiImpactAnalysis: analysis.impactAnalysis, aiActionItems: analysis.actionItems, aiEstimatedEffort: analysis.estimatedEffort, aiSuggestedPriority: analysis.suggestedPriority, aiThreadId: threadId, }); // Send email notifications try { await ctx.runAction(internal.emails.feedbackEmails.sendFeedbackNotifications, { feedbackId: args.feedbackId, analysis: { summary: analysis.summary, impactAnalysis: analysis.impactAnalysis, actionItems: analysis.actionItems, estimatedEffort: analysis.estimatedEffort, suggestedPriority: analysis.suggestedPriority, }, resendApiKey: args.resendApiKey, resendFromEmail: args.resendFromEmail, }); // Mark notifications as sent await ctx.runMutation(internal.feedback.markNotificationsSent, { feedbackId: args.feedbackId, }); } catch { console.log("Email notifications not configured or failed"); } return { success: true }; }, }); /** * Public wrapper for processFeedback that can be called from parent app. * Accepts optional API keys since Convex components don't inherit parent env vars. */ export const processFeedbackPublic = action({ args: { feedbackId: v.id("feedback"), openRouterApiKey: v.optional(v.string()), resendApiKey: v.optional(v.string()), resendFromEmail: v.optional(v.string()), }, returns: v.object({ success: v.boolean(), error: v.optional(v.string()), }), handler: async (ctx, args): Promise<{ success: boolean; error?: string }> => { // Delegate to the internal action return await ctx.runAction(internal.agents.feedbackAgent.processFeedback, { feedbackId: args.feedbackId, openRouterApiKey: args.openRouterApiKey, resendApiKey: args.resendApiKey, resendFromEmail: args.resendFromEmail, }); }, });