@fatagnus/convex-feedback 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +346 -4
  2. package/dist/convex/_generated/api.d.ts +2310 -0
  3. package/dist/convex/_generated/api.d.ts.map +1 -0
  4. package/dist/convex/_generated/api.js +32 -0
  5. package/dist/convex/_generated/api.js.map +1 -0
  6. package/dist/convex/_generated/dataModel.d.ts +46 -0
  7. package/dist/convex/_generated/dataModel.d.ts.map +1 -0
  8. package/dist/convex/_generated/dataModel.js +11 -0
  9. package/dist/convex/_generated/dataModel.js.map +1 -0
  10. package/dist/convex/_generated/server.d.ts +121 -0
  11. package/dist/convex/_generated/server.d.ts.map +1 -0
  12. package/dist/convex/_generated/server.js +78 -0
  13. package/dist/convex/_generated/server.js.map +1 -0
  14. package/dist/convex/agents/bugReportAgent.d.ts +35 -9
  15. package/dist/convex/agents/bugReportAgent.d.ts.map +1 -1
  16. package/dist/convex/agents/bugReportAgent.js +61 -27
  17. package/dist/convex/agents/bugReportAgent.js.map +1 -1
  18. package/dist/convex/agents/feedbackAgent.d.ts +35 -9
  19. package/dist/convex/agents/feedbackAgent.d.ts.map +1 -1
  20. package/dist/convex/agents/feedbackAgent.js +61 -27
  21. package/dist/convex/agents/feedbackAgent.js.map +1 -1
  22. package/dist/convex/agents/feedbackInterviewAgent.d.ts +76 -0
  23. package/dist/convex/agents/feedbackInterviewAgent.d.ts.map +1 -0
  24. package/dist/convex/agents/feedbackInterviewAgent.js +812 -0
  25. package/dist/convex/agents/feedbackInterviewAgent.js.map +1 -0
  26. package/dist/convex/agents/index.d.ts +9 -0
  27. package/dist/convex/agents/index.d.ts.map +1 -0
  28. package/dist/convex/agents/index.js +13 -0
  29. package/dist/convex/agents/index.js.map +1 -0
  30. package/dist/convex/apiKeys.d.ts +56 -0
  31. package/dist/convex/apiKeys.d.ts.map +1 -0
  32. package/dist/convex/apiKeys.js +197 -0
  33. package/dist/convex/apiKeys.js.map +1 -0
  34. package/dist/convex/bugReports.d.ts +131 -11
  35. package/dist/convex/bugReports.d.ts.map +1 -1
  36. package/dist/convex/bugReports.js +138 -10
  37. package/dist/convex/bugReports.js.map +1 -1
  38. package/dist/convex/emails/bugReportEmails.d.ts +31 -2
  39. package/dist/convex/emails/bugReportEmails.d.ts.map +1 -1
  40. package/dist/convex/emails/bugReportEmails.js +6 -3
  41. package/dist/convex/emails/bugReportEmails.js.map +1 -1
  42. package/dist/convex/emails/feedbackEmails.d.ts +31 -2
  43. package/dist/convex/emails/feedbackEmails.d.ts.map +1 -1
  44. package/dist/convex/emails/feedbackEmails.js +6 -3
  45. package/dist/convex/emails/feedbackEmails.js.map +1 -1
  46. package/dist/convex/feedback.d.ts +132 -11
  47. package/dist/convex/feedback.d.ts.map +1 -1
  48. package/dist/convex/feedback.js +146 -9
  49. package/dist/convex/feedback.js.map +1 -1
  50. package/dist/convex/http.d.ts +39 -0
  51. package/dist/convex/http.d.ts.map +1 -0
  52. package/dist/convex/http.js +467 -0
  53. package/dist/convex/http.js.map +1 -0
  54. package/dist/convex/index.d.ts +8 -1
  55. package/dist/convex/index.d.ts.map +1 -1
  56. package/dist/convex/index.js +8 -1
  57. package/dist/convex/index.js.map +1 -1
  58. package/dist/convex/inputRequests.d.ts +118 -0
  59. package/dist/convex/inputRequests.d.ts.map +1 -0
  60. package/dist/convex/inputRequests.js +141 -0
  61. package/dist/convex/inputRequests.js.map +1 -0
  62. package/dist/convex/prompts.d.ts +110 -0
  63. package/dist/convex/prompts.d.ts.map +1 -0
  64. package/dist/convex/prompts.js +403 -0
  65. package/dist/convex/prompts.js.map +1 -0
  66. package/dist/convex/schema.d.ts +310 -54
  67. package/dist/convex/schema.d.ts.map +1 -1
  68. package/dist/convex/schema.js +120 -2
  69. package/dist/convex/schema.js.map +1 -1
  70. package/dist/convex/supportTeams.d.ts +69 -7
  71. package/dist/convex/supportTeams.d.ts.map +1 -1
  72. package/dist/index.d.ts +28 -2
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +25 -2
  75. package/dist/index.js.map +1 -1
  76. package/dist/types.d.ts +35 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/package.json +12 -5
  79. package/src/convex/_generated/api.ts +1 -0
  80. package/src/convex/agents/feedbackInterviewAgent.ts +6 -12
  81. package/src/convex/apiKeys.test.ts +79 -0
  82. package/src/convex/apiKeys.ts +223 -0
  83. package/src/convex/bugReports.ts +126 -1
  84. package/src/convex/feedback.ts +134 -1
  85. package/src/convex/http.test.ts +76 -0
  86. package/src/convex/http.ts +630 -0
  87. package/src/convex/index.ts +11 -0
  88. package/src/convex/prompts.test.ts +185 -0
  89. package/src/convex/prompts.ts +605 -0
  90. package/src/convex/schema.ts +52 -2
  91. package/src/convex/ticketNumbers.ts +4 -0
  92. package/src/convex/tsconfig.json +24 -0
  93. package/src/index.ts +33 -1
  94. package/src/types.ts +38 -0
  95. package/dist/convex/convex.config.d.ts +0 -3
  96. package/dist/convex/convex.config.d.ts.map +0 -1
  97. package/dist/convex/convex.config.js +0 -6
  98. package/dist/convex/convex.config.js.map +0 -1
@@ -0,0 +1,812 @@
1
+ /**
2
+ * Feedback Interview Agent
3
+ *
4
+ * Conducts AI-powered interviews to help users articulate bug reports
5
+ * and feedback through a conversational experience.
6
+ *
7
+ * Features:
8
+ * - Context-aware questions based on app information
9
+ * - Human-in-the-loop input collection
10
+ * - Generates well-structured reports from conversations
11
+ */
12
+ import { v } from "convex/values";
13
+ import { Agent, createTool, createThread } from "@convex-dev/agent";
14
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
15
+ import { z } from "zod";
16
+ import { components, internal, api } from "../_generated/api";
17
+ import { action, query, internalMutation } from "../_generated/server";
18
+ // Helper to create OpenRouter provider with a specific API key
19
+ // This is created dynamically because Convex components don't inherit parent env vars
20
+ function createOpenRouterProvider(apiKey) {
21
+ return createOpenAICompatible({
22
+ name: "openrouter",
23
+ baseURL: "https://openrouter.ai/api/v1",
24
+ headers: {
25
+ Authorization: `Bearer ${apiKey}`,
26
+ },
27
+ });
28
+ }
29
+ // Agent name constant
30
+ const AGENT_NAME = "Feedback Interview Agent";
31
+ // ============================================================================
32
+ // Tools
33
+ // ============================================================================
34
+ /**
35
+ * Tool to request user input during the interview
36
+ */
37
+ const requestUserInput = createTool({
38
+ description: `Request input from the user during the interview. Use this to gather information about their bug report or feedback.
39
+ Choose the appropriate input type:
40
+ - "text": For open-ended questions
41
+ - "choice": When user should pick from specific options
42
+ - "form": When you need multiple pieces of information at once`,
43
+ args: z.object({
44
+ inputType: z.enum(["text", "choice", "form"]).describe("Type of input"),
45
+ prompt: z.string().describe("The question to ask"),
46
+ placeholder: z.string().optional().describe("Placeholder for text input"),
47
+ multiline: z.boolean().optional().describe("Allow multiline text"),
48
+ options: z.array(z.object({
49
+ label: z.string(),
50
+ value: z.string(),
51
+ description: z.string().optional(),
52
+ })).optional().describe("Options for choice input"),
53
+ fields: z.array(z.object({
54
+ name: z.string(),
55
+ label: z.string(),
56
+ type: z.enum(["text", "number", "email", "textarea"]),
57
+ required: z.boolean(),
58
+ placeholder: z.string().optional(),
59
+ })).optional().describe("Fields for form input"),
60
+ }),
61
+ handler: async (ctx, args) => {
62
+ const config = {};
63
+ if (args.inputType === "text") {
64
+ if (args.placeholder)
65
+ config.placeholder = args.placeholder;
66
+ if (args.multiline !== undefined)
67
+ config.multiline = args.multiline;
68
+ }
69
+ else if (args.inputType === "choice") {
70
+ if (!args.options || args.options.length < 2) {
71
+ return "Error: Choice input requires at least 2 options";
72
+ }
73
+ config.options = args.options;
74
+ }
75
+ else if (args.inputType === "form") {
76
+ if (!args.fields || args.fields.length === 0) {
77
+ return "Error: Form input requires at least 1 field";
78
+ }
79
+ config.fields = args.fields;
80
+ }
81
+ const toolCallId = `feedback-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
82
+ if (!ctx.threadId) {
83
+ return "Error: No active thread.";
84
+ }
85
+ const requestId = await ctx.runMutation(internal.inputRequests.createRequest, {
86
+ threadId: ctx.threadId,
87
+ toolCallId,
88
+ inputType: args.inputType,
89
+ prompt: args.prompt,
90
+ config: Object.keys(config).length > 0 ? config : undefined,
91
+ agentName: AGENT_NAME,
92
+ userId: ctx.userId,
93
+ });
94
+ return JSON.stringify({
95
+ status: "WAITING_FOR_USER_INPUT",
96
+ requestId: requestId,
97
+ message: `Waiting for user response: ${args.prompt}`,
98
+ });
99
+ },
100
+ });
101
+ /**
102
+ * Tool to generate the final bug report
103
+ */
104
+ const generateBugReport = createTool({
105
+ description: "Generate the final bug report from the interview. Call this when you have gathered enough information about the bug.",
106
+ args: z.object({
107
+ title: z.string().describe("A clear, concise title for the bug report"),
108
+ description: z.string().describe("Detailed description of the bug"),
109
+ severity: z.enum(["low", "medium", "high", "critical"]).describe("Bug severity"),
110
+ reproductionSteps: z.array(z.string()).optional().describe("Steps to reproduce the bug"),
111
+ featureArea: z.string().optional().describe("Which feature area is affected"),
112
+ }),
113
+ handler: async (ctx, args) => {
114
+ // Get the session by threadId (threadId is available in tool context)
115
+ if (!ctx.threadId) {
116
+ console.warn("generateBugReport: No threadId in context, session not updated");
117
+ return JSON.stringify({
118
+ status: "REPORT_GENERATED",
119
+ report: {
120
+ title: args.title,
121
+ description: args.description,
122
+ severity: args.severity,
123
+ reproductionSteps: args.reproductionSteps,
124
+ featureArea: args.featureArea,
125
+ },
126
+ message: "Bug report has been generated. The interview is complete.",
127
+ });
128
+ }
129
+ // Query the session by threadId and update it
130
+ const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, {
131
+ threadId: ctx.threadId,
132
+ });
133
+ if (session) {
134
+ await ctx.runMutation(internal.agents.feedbackInterviewAgent.updateSession, {
135
+ sessionId: session._id,
136
+ generatedTitle: args.title,
137
+ generatedDescription: args.description,
138
+ generatedSeverity: args.severity,
139
+ generatedFeatureArea: args.featureArea,
140
+ generatedReproSteps: args.reproductionSteps,
141
+ isComplete: true,
142
+ });
143
+ }
144
+ else {
145
+ console.warn("generateBugReport: No session found for threadId", ctx.threadId);
146
+ }
147
+ return JSON.stringify({
148
+ status: "REPORT_GENERATED",
149
+ report: {
150
+ title: args.title,
151
+ description: args.description,
152
+ severity: args.severity,
153
+ reproductionSteps: args.reproductionSteps,
154
+ featureArea: args.featureArea,
155
+ },
156
+ message: "Bug report has been generated. The interview is complete.",
157
+ });
158
+ },
159
+ });
160
+ /**
161
+ * Tool to generate the final feedback
162
+ */
163
+ const generateFeedback = createTool({
164
+ description: "Generate the final feedback from the interview. Call this when you have gathered enough information about the user's suggestion or request.",
165
+ args: z.object({
166
+ title: z.string().describe("A clear, concise title for the feedback"),
167
+ description: z.string().describe("Detailed description of the feedback"),
168
+ type: z.enum(["feature_request", "change_request", "general"]).describe("Type of feedback"),
169
+ priority: z.enum(["nice_to_have", "important", "critical"]).describe("Priority level"),
170
+ featureArea: z.string().optional().describe("Which feature area this relates to"),
171
+ }),
172
+ handler: async (ctx, args) => {
173
+ // Get the session by threadId (threadId is available in tool context)
174
+ if (!ctx.threadId) {
175
+ console.warn("generateFeedback: No threadId in context, session not updated");
176
+ return JSON.stringify({
177
+ status: "FEEDBACK_GENERATED",
178
+ report: {
179
+ title: args.title,
180
+ description: args.description,
181
+ type: args.type,
182
+ priority: args.priority,
183
+ featureArea: args.featureArea,
184
+ },
185
+ message: "Feedback has been generated. The interview is complete.",
186
+ });
187
+ }
188
+ // Query the session by threadId and update it
189
+ const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, {
190
+ threadId: ctx.threadId,
191
+ });
192
+ if (session) {
193
+ await ctx.runMutation(internal.agents.feedbackInterviewAgent.updateSession, {
194
+ sessionId: session._id,
195
+ generatedTitle: args.title,
196
+ generatedDescription: args.description,
197
+ generatedFeedbackType: args.type,
198
+ generatedPriority: args.priority,
199
+ generatedFeatureArea: args.featureArea,
200
+ isComplete: true,
201
+ });
202
+ }
203
+ else {
204
+ console.warn("generateFeedback: No session found for threadId", ctx.threadId);
205
+ }
206
+ return JSON.stringify({
207
+ status: "FEEDBACK_GENERATED",
208
+ report: {
209
+ title: args.title,
210
+ description: args.description,
211
+ type: args.type,
212
+ priority: args.priority,
213
+ featureArea: args.featureArea,
214
+ },
215
+ message: "Feedback has been generated. The interview is complete.",
216
+ });
217
+ },
218
+ });
219
+ // ============================================================================
220
+ // Internal Mutations
221
+ // ============================================================================
222
+ /**
223
+ * Create an interview session
224
+ */
225
+ export const createSession = internalMutation({
226
+ args: {
227
+ threadId: v.string(),
228
+ reportType: v.union(v.literal("bug"), v.literal("feedback")),
229
+ reporterType: v.union(v.literal("staff"), v.literal("customer")),
230
+ reporterId: v.string(),
231
+ reporterEmail: v.string(),
232
+ reporterName: v.string(),
233
+ context: v.optional(v.string()),
234
+ },
235
+ returns: v.id("interviewSessions"),
236
+ handler: async (ctx, args) => {
237
+ const now = Date.now();
238
+ return await ctx.db.insert("interviewSessions", {
239
+ threadId: args.threadId,
240
+ reportType: args.reportType,
241
+ reporterType: args.reporterType,
242
+ reporterId: args.reporterId,
243
+ reporterEmail: args.reporterEmail,
244
+ reporterName: args.reporterName,
245
+ context: args.context,
246
+ isComplete: false,
247
+ createdAt: now,
248
+ updatedAt: now,
249
+ });
250
+ },
251
+ });
252
+ /**
253
+ * Update an interview session with generated report
254
+ */
255
+ export const updateSession = internalMutation({
256
+ args: {
257
+ sessionId: v.id("interviewSessions"),
258
+ generatedTitle: v.optional(v.string()),
259
+ generatedDescription: v.optional(v.string()),
260
+ generatedSeverity: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical"))),
261
+ generatedFeedbackType: v.optional(v.union(v.literal("feature_request"), v.literal("change_request"), v.literal("general"))),
262
+ generatedPriority: v.optional(v.union(v.literal("nice_to_have"), v.literal("important"), v.literal("critical"))),
263
+ generatedFeatureArea: v.optional(v.string()),
264
+ generatedReproSteps: v.optional(v.array(v.string())),
265
+ isComplete: v.optional(v.boolean()),
266
+ },
267
+ returns: v.null(),
268
+ handler: async (ctx, args) => {
269
+ const { sessionId, ...updates } = args;
270
+ await ctx.db.patch(sessionId, {
271
+ ...updates,
272
+ updatedAt: Date.now(),
273
+ });
274
+ return null;
275
+ },
276
+ });
277
+ // ============================================================================
278
+ // Public Queries
279
+ // ============================================================================
280
+ /**
281
+ * Get an interview session by thread ID
282
+ */
283
+ export const getSessionByThread = query({
284
+ args: {
285
+ threadId: v.string(),
286
+ },
287
+ returns: v.union(v.object({
288
+ _id: v.id("interviewSessions"),
289
+ _creationTime: v.number(),
290
+ threadId: v.string(),
291
+ reportType: v.union(v.literal("bug"), v.literal("feedback")),
292
+ reporterType: v.union(v.literal("staff"), v.literal("customer")),
293
+ reporterId: v.string(),
294
+ reporterEmail: v.string(),
295
+ reporterName: v.string(),
296
+ context: v.optional(v.string()),
297
+ generatedTitle: v.optional(v.string()),
298
+ generatedDescription: v.optional(v.string()),
299
+ generatedSeverity: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical"))),
300
+ generatedFeedbackType: v.optional(v.union(v.literal("feature_request"), v.literal("change_request"), v.literal("general"))),
301
+ generatedPriority: v.optional(v.union(v.literal("nice_to_have"), v.literal("important"), v.literal("critical"))),
302
+ generatedFeatureArea: v.optional(v.string()),
303
+ generatedReproSteps: v.optional(v.array(v.string())),
304
+ isComplete: v.boolean(),
305
+ createdAt: v.number(),
306
+ updatedAt: v.number(),
307
+ }), v.null()),
308
+ handler: async (ctx, args) => {
309
+ return await ctx.db
310
+ .query("interviewSessions")
311
+ .withIndex("by_thread", (q) => q.eq("threadId", args.threadId))
312
+ .first();
313
+ },
314
+ });
315
+ // ============================================================================
316
+ // Public Actions
317
+ // ============================================================================
318
+ /**
319
+ * Build dynamic instructions based on context
320
+ */
321
+ function buildBugInstructions(context) {
322
+ let instructions = `You are a helpful assistant that interviews users to gather detailed information about bugs they've encountered.
323
+
324
+ Your goal is to understand the bug thoroughly and generate a high-quality bug report.`;
325
+ if (context?.appName) {
326
+ instructions += `\n\n## Application Context\nYou are gathering bug reports for **${context.appName}**.`;
327
+ if (context.appDescription) {
328
+ instructions += ` ${context.appDescription}`;
329
+ }
330
+ }
331
+ if (context?.featureAreas && context.featureAreas.length > 0) {
332
+ instructions += `\n\n## Feature Areas\nThe application has these feature areas:\n`;
333
+ context.featureAreas.forEach((area) => {
334
+ instructions += `- **${area.name}**${area.description ? `: ${area.description}` : ""}\n`;
335
+ });
336
+ instructions += `\nWhen appropriate, ask the user which feature area their bug relates to using a choice input.`;
337
+ }
338
+ if (context?.knownIssues && context.knownIssues.length > 0) {
339
+ instructions += `\n\n## Known Issues\nThese are known issues. If the user's bug matches any, mention it:\n`;
340
+ context.knownIssues.forEach((issue) => {
341
+ instructions += `- **${issue.title}**${issue.description ? `: ${issue.description}` : ""}\n`;
342
+ });
343
+ }
344
+ instructions += `\n\n## Interview Flow
345
+
346
+ 1. **Greet briefly** and ask what went wrong (the bug they encountered).
347
+
348
+ 2. **Understand the bug**:
349
+ - What happened? (the actual behavior)
350
+ - What did they expect to happen? (expected behavior)
351
+ - How often does this occur? (always, sometimes, once)
352
+
353
+ 3. **Get reproduction info**:
354
+ - What steps led to this bug?
355
+ - Can they reliably reproduce it?`;
356
+ if (context?.customQuestions?.bug && context.customQuestions.bug.length > 0) {
357
+ instructions += `\n\n4. **Custom questions** (ask these if relevant):\n`;
358
+ context.customQuestions.bug.forEach((q) => {
359
+ instructions += ` - ${q}\n`;
360
+ });
361
+ instructions += `\n5. **Assess impact**:`;
362
+ }
363
+ else {
364
+ instructions += `\n\n4. **Assess impact**:`;
365
+ }
366
+ instructions += `
367
+ - How does this affect their work?
368
+ - How urgent is this for them?
369
+
370
+ 5. **Generate the report** using the generateBugReport tool when you have enough information.`;
371
+ if (context?.additionalInstructions) {
372
+ instructions += `\n\n## Additional Guidelines\n${context.additionalInstructions}`;
373
+ }
374
+ instructions += `\n\n## Guidelines
375
+ - Be conversational but efficient - aim for 3-5 exchanges
376
+ - Use choice inputs when there are clear options
377
+ - Use text inputs for open-ended questions
378
+ - Don't ask for technical details the user might not know
379
+ - Focus on understanding the user experience
380
+ - Generate the report as soon as you have enough info`;
381
+ return instructions;
382
+ }
383
+ function buildFeedbackInstructions(context) {
384
+ let instructions = `You are a helpful assistant that interviews users to gather detailed feedback and suggestions.
385
+
386
+ Your goal is to understand their idea thoroughly and generate well-structured feedback.`;
387
+ if (context?.appName) {
388
+ instructions += `\n\n## Application Context\nYou are gathering feedback for **${context.appName}**.`;
389
+ if (context.appDescription) {
390
+ instructions += ` ${context.appDescription}`;
391
+ }
392
+ }
393
+ if (context?.featureAreas && context.featureAreas.length > 0) {
394
+ instructions += `\n\n## Feature Areas\nThe application has these feature areas:\n`;
395
+ context.featureAreas.forEach((area) => {
396
+ instructions += `- **${area.name}**${area.description ? `: ${area.description}` : ""}\n`;
397
+ });
398
+ instructions += `\nWhen appropriate, ask which feature area their feedback relates to using a choice input.`;
399
+ }
400
+ instructions += `\n\n## Interview Flow
401
+
402
+ 1. **Greet briefly** and ask about their idea or suggestion.
403
+
404
+ 2. **Understand the need**:
405
+ - What problem does this solve?
406
+ - What's their current workaround (if any)?`;
407
+ if (context?.customQuestions?.feedback && context.customQuestions.feedback.length > 0) {
408
+ instructions += `\n\n3. **Custom questions** (ask these if relevant):\n`;
409
+ context.customQuestions.feedback.forEach((q) => {
410
+ instructions += ` - ${q}\n`;
411
+ });
412
+ instructions += `\n4. **Clarify the request**:`;
413
+ }
414
+ else {
415
+ instructions += `\n\n3. **Clarify the request**:`;
416
+ }
417
+ instructions += `
418
+ - What would the ideal solution look like?
419
+ - Is this a new feature, change to existing, or general feedback?
420
+
421
+ 4. **Assess importance**:
422
+ - How important is this to them?
423
+ - Who else might benefit?
424
+
425
+ 5. **Generate the feedback** using the generateFeedback tool when you have enough information.`;
426
+ if (context?.additionalInstructions) {
427
+ instructions += `\n\n## Additional Guidelines\n${context.additionalInstructions}`;
428
+ }
429
+ instructions += `\n\n## Guidelines
430
+ - Be conversational but efficient - aim for 3-5 exchanges
431
+ - Use choice inputs when there are clear options
432
+ - Use text inputs for open-ended questions
433
+ - Help users articulate their ideas clearly
434
+ - Focus on understanding the value and impact
435
+ - Generate the feedback as soon as you have enough info`;
436
+ return instructions;
437
+ }
438
+ /**
439
+ * Start a bug report interview
440
+ */
441
+ export const startBugInterview = action({
442
+ args: {
443
+ openRouterApiKey: v.string(),
444
+ reporterType: v.union(v.literal("staff"), v.literal("customer")),
445
+ reporterId: v.string(),
446
+ reporterEmail: v.string(),
447
+ reporterName: v.string(),
448
+ context: v.optional(v.object({
449
+ appName: v.optional(v.string()),
450
+ appDescription: v.optional(v.string()),
451
+ featureAreas: v.optional(v.array(v.object({
452
+ name: v.string(),
453
+ description: v.optional(v.string()),
454
+ }))),
455
+ knownIssues: v.optional(v.array(v.object({
456
+ title: v.string(),
457
+ description: v.optional(v.string()),
458
+ }))),
459
+ customQuestions: v.optional(v.object({
460
+ bug: v.optional(v.array(v.string())),
461
+ feedback: v.optional(v.array(v.string())),
462
+ })),
463
+ additionalInstructions: v.optional(v.string()),
464
+ })),
465
+ },
466
+ returns: v.object({
467
+ threadId: v.string(),
468
+ sessionId: v.string(),
469
+ response: v.string(),
470
+ waitingForInput: v.boolean(),
471
+ pendingRequest: v.optional(v.object({
472
+ requestId: v.string(),
473
+ inputType: v.string(),
474
+ prompt: v.string(),
475
+ config: v.optional(v.any()),
476
+ })),
477
+ }),
478
+ handler: async (ctx, args) => {
479
+ // Create OpenRouter provider with the passed API key
480
+ const openrouter = createOpenRouterProvider(args.openRouterApiKey);
481
+ // Create a thread for the interview
482
+ const threadId = await createThread(ctx, components.agent, {
483
+ title: `Bug Report Interview: ${args.reporterName}`,
484
+ });
485
+ // Create interview session
486
+ const sessionId = await ctx.runMutation(internal.agents.feedbackInterviewAgent.createSession, {
487
+ threadId,
488
+ reportType: "bug",
489
+ reporterType: args.reporterType,
490
+ reporterId: args.reporterId,
491
+ reporterEmail: args.reporterEmail,
492
+ reporterName: args.reporterName,
493
+ context: args.context ? JSON.stringify(args.context) : undefined,
494
+ });
495
+ // Build dynamic instructions based on context
496
+ const dynamicInstructions = buildBugInstructions(args.context);
497
+ // Create agent with dynamic instructions
498
+ const dynamicAgent = new Agent(components.agent, {
499
+ name: "Bug Report Interview Agent",
500
+ languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"),
501
+ instructions: dynamicInstructions,
502
+ tools: {
503
+ requestUserInput,
504
+ generateBugReport,
505
+ },
506
+ maxSteps: 30,
507
+ });
508
+ // Start the interview
509
+ // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx
510
+ let result;
511
+ try {
512
+ result = await dynamicAgent.generateText(ctx, { threadId }, { prompt: "Start the bug report interview. Greet the user briefly and ask what bug or issue they encountered." });
513
+ }
514
+ catch (error) {
515
+ // Provide more descriptive error messages based on the error type
516
+ const errorMessage = error instanceof Error ? error.message : String(error);
517
+ if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
518
+ throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY.");
519
+ }
520
+ else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) {
521
+ throw new Error("AI service rate limited. Please try again in a few moments.");
522
+ }
523
+ else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) {
524
+ throw new Error("AI service temporarily unavailable. Please try again later.");
525
+ }
526
+ else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
527
+ throw new Error("AI service request timed out. Please try again.");
528
+ }
529
+ throw new Error(`Failed to start interview: ${errorMessage}`);
530
+ }
531
+ // Check for pending input request
532
+ const pendingRequest = await ctx.runQuery(api.inputRequests.getPendingForThread, { threadId });
533
+ if (pendingRequest) {
534
+ return {
535
+ threadId,
536
+ sessionId,
537
+ response: result.text,
538
+ waitingForInput: true,
539
+ pendingRequest: {
540
+ requestId: pendingRequest._id,
541
+ inputType: pendingRequest.inputType,
542
+ prompt: pendingRequest.prompt,
543
+ config: pendingRequest.config,
544
+ },
545
+ };
546
+ }
547
+ return {
548
+ threadId,
549
+ sessionId,
550
+ response: result.text,
551
+ waitingForInput: false,
552
+ };
553
+ },
554
+ });
555
+ /**
556
+ * Start a feedback interview
557
+ */
558
+ export const startFeedbackInterview = action({
559
+ args: {
560
+ openRouterApiKey: v.string(),
561
+ reporterType: v.union(v.literal("staff"), v.literal("customer")),
562
+ reporterId: v.string(),
563
+ reporterEmail: v.string(),
564
+ reporterName: v.string(),
565
+ context: v.optional(v.object({
566
+ appName: v.optional(v.string()),
567
+ appDescription: v.optional(v.string()),
568
+ featureAreas: v.optional(v.array(v.object({
569
+ name: v.string(),
570
+ description: v.optional(v.string()),
571
+ }))),
572
+ knownIssues: v.optional(v.array(v.object({
573
+ title: v.string(),
574
+ description: v.optional(v.string()),
575
+ }))),
576
+ customQuestions: v.optional(v.object({
577
+ bug: v.optional(v.array(v.string())),
578
+ feedback: v.optional(v.array(v.string())),
579
+ })),
580
+ additionalInstructions: v.optional(v.string()),
581
+ })),
582
+ },
583
+ returns: v.object({
584
+ threadId: v.string(),
585
+ sessionId: v.string(),
586
+ response: v.string(),
587
+ waitingForInput: v.boolean(),
588
+ pendingRequest: v.optional(v.object({
589
+ requestId: v.string(),
590
+ inputType: v.string(),
591
+ prompt: v.string(),
592
+ config: v.optional(v.any()),
593
+ })),
594
+ }),
595
+ handler: async (ctx, args) => {
596
+ // Create OpenRouter provider with the passed API key
597
+ const openrouter = createOpenRouterProvider(args.openRouterApiKey);
598
+ // Create a thread for the interview
599
+ const threadId = await createThread(ctx, components.agent, {
600
+ title: `Feedback Interview: ${args.reporterName}`,
601
+ });
602
+ // Create interview session
603
+ const sessionId = await ctx.runMutation(internal.agents.feedbackInterviewAgent.createSession, {
604
+ threadId,
605
+ reportType: "feedback",
606
+ reporterType: args.reporterType,
607
+ reporterId: args.reporterId,
608
+ reporterEmail: args.reporterEmail,
609
+ reporterName: args.reporterName,
610
+ context: args.context ? JSON.stringify(args.context) : undefined,
611
+ });
612
+ // Build dynamic instructions based on context
613
+ const dynamicInstructions = buildFeedbackInstructions(args.context);
614
+ // Create agent with dynamic instructions
615
+ const dynamicAgent = new Agent(components.agent, {
616
+ name: "Feedback Interview Agent",
617
+ languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"),
618
+ instructions: dynamicInstructions,
619
+ tools: {
620
+ requestUserInput,
621
+ generateFeedback,
622
+ },
623
+ maxSteps: 30,
624
+ });
625
+ // Start the interview
626
+ // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx
627
+ let result;
628
+ try {
629
+ result = await dynamicAgent.generateText(ctx, { threadId }, { prompt: "Start the feedback interview. Greet the user briefly and ask about their idea or suggestion." });
630
+ }
631
+ catch (error) {
632
+ // Provide more descriptive error messages based on the error type
633
+ const errorMessage = error instanceof Error ? error.message : String(error);
634
+ if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
635
+ throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY.");
636
+ }
637
+ else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) {
638
+ throw new Error("AI service rate limited. Please try again in a few moments.");
639
+ }
640
+ else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) {
641
+ throw new Error("AI service temporarily unavailable. Please try again later.");
642
+ }
643
+ else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
644
+ throw new Error("AI service request timed out. Please try again.");
645
+ }
646
+ throw new Error(`Failed to start interview: ${errorMessage}`);
647
+ }
648
+ // Check for pending input request
649
+ const pendingRequest = await ctx.runQuery(api.inputRequests.getPendingForThread, { threadId });
650
+ if (pendingRequest) {
651
+ return {
652
+ threadId,
653
+ sessionId,
654
+ response: result.text,
655
+ waitingForInput: true,
656
+ pendingRequest: {
657
+ requestId: pendingRequest._id,
658
+ inputType: pendingRequest.inputType,
659
+ prompt: pendingRequest.prompt,
660
+ config: pendingRequest.config,
661
+ },
662
+ };
663
+ }
664
+ return {
665
+ threadId,
666
+ sessionId,
667
+ response: result.text,
668
+ waitingForInput: false,
669
+ };
670
+ },
671
+ });
672
+ /**
673
+ * Continue the interview after user provides input
674
+ */
675
+ export const continueInterview = action({
676
+ args: {
677
+ openRouterApiKey: v.string(),
678
+ threadId: v.string(),
679
+ sessionId: v.string(),
680
+ requestId: v.id("feedbackInputRequests"),
681
+ response: v.string(),
682
+ context: v.optional(v.object({
683
+ appName: v.optional(v.string()),
684
+ appDescription: v.optional(v.string()),
685
+ featureAreas: v.optional(v.array(v.object({
686
+ name: v.string(),
687
+ description: v.optional(v.string()),
688
+ }))),
689
+ knownIssues: v.optional(v.array(v.object({
690
+ title: v.string(),
691
+ description: v.optional(v.string()),
692
+ }))),
693
+ customQuestions: v.optional(v.object({
694
+ bug: v.optional(v.array(v.string())),
695
+ feedback: v.optional(v.array(v.string())),
696
+ })),
697
+ additionalInstructions: v.optional(v.string()),
698
+ })),
699
+ reportType: v.union(v.literal("bug"), v.literal("feedback")),
700
+ },
701
+ returns: v.object({
702
+ response: v.string(),
703
+ waitingForInput: v.boolean(),
704
+ isComplete: v.boolean(),
705
+ generatedReport: v.optional(v.object({
706
+ title: v.string(),
707
+ description: v.string(),
708
+ severity: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high"), v.literal("critical"))),
709
+ type: v.optional(v.union(v.literal("feature_request"), v.literal("change_request"), v.literal("general"))),
710
+ priority: v.optional(v.union(v.literal("nice_to_have"), v.literal("important"), v.literal("critical"))),
711
+ featureArea: v.optional(v.string()),
712
+ reproductionSteps: v.optional(v.array(v.string())),
713
+ })),
714
+ pendingRequest: v.optional(v.object({
715
+ requestId: v.string(),
716
+ inputType: v.string(),
717
+ prompt: v.string(),
718
+ config: v.optional(v.any()),
719
+ })),
720
+ }),
721
+ handler: async (ctx, args) => {
722
+ // Create OpenRouter provider with the passed API key
723
+ const openrouter = createOpenRouterProvider(args.openRouterApiKey);
724
+ // Submit the response
725
+ await ctx.runMutation(api.inputRequests.submitResponse, {
726
+ requestId: args.requestId,
727
+ response: args.response,
728
+ });
729
+ // Build dynamic instructions based on context and report type
730
+ const dynamicInstructions = args.reportType === "bug"
731
+ ? buildBugInstructions(args.context)
732
+ : buildFeedbackInstructions(args.context);
733
+ // Create agent with appropriate tools
734
+ const tools = args.reportType === "bug"
735
+ ? { requestUserInput, generateBugReport }
736
+ : { requestUserInput, generateFeedback };
737
+ const dynamicAgent = new Agent(components.agent, {
738
+ name: args.reportType === "bug" ? "Bug Report Interview Agent" : "Feedback Interview Agent",
739
+ languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"),
740
+ instructions: dynamicInstructions,
741
+ tools,
742
+ maxSteps: 30,
743
+ });
744
+ // Continue the agent
745
+ // Note: Tools look up session by threadId, so we don't need to pass sessionId via customCtx
746
+ let result;
747
+ try {
748
+ result = await dynamicAgent.generateText(ctx, { threadId: args.threadId }, {
749
+ prompt: `The user responded: ${args.response}
750
+
751
+ Please continue the interview based on their response.`,
752
+ });
753
+ }
754
+ catch (error) {
755
+ // Provide more descriptive error messages based on the error type
756
+ const errorMessage = error instanceof Error ? error.message : String(error);
757
+ if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
758
+ throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY.");
759
+ }
760
+ else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) {
761
+ throw new Error("AI service rate limited. Please try again in a few moments.");
762
+ }
763
+ else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) {
764
+ throw new Error("AI service temporarily unavailable. Please try again later.");
765
+ }
766
+ else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
767
+ throw new Error("AI service request timed out. Please try again.");
768
+ }
769
+ throw new Error(`Failed to continue interview: ${errorMessage}`);
770
+ }
771
+ // Check if interview is complete (session has generated report)
772
+ const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, {
773
+ threadId: args.threadId,
774
+ });
775
+ const isComplete = session?.isComplete ?? false;
776
+ let generatedReport;
777
+ if (isComplete && session) {
778
+ generatedReport = {
779
+ title: session.generatedTitle ?? "",
780
+ description: session.generatedDescription ?? "",
781
+ severity: session.generatedSeverity,
782
+ type: session.generatedFeedbackType,
783
+ priority: session.generatedPriority,
784
+ featureArea: session.generatedFeatureArea,
785
+ reproductionSteps: session.generatedReproSteps,
786
+ };
787
+ }
788
+ // Check for new pending request
789
+ const pendingRequest = await ctx.runQuery(api.inputRequests.getPendingForThread, { threadId: args.threadId });
790
+ if (pendingRequest) {
791
+ return {
792
+ response: result.text,
793
+ waitingForInput: true,
794
+ isComplete,
795
+ generatedReport,
796
+ pendingRequest: {
797
+ requestId: pendingRequest._id,
798
+ inputType: pendingRequest.inputType,
799
+ prompt: pendingRequest.prompt,
800
+ config: pendingRequest.config,
801
+ },
802
+ };
803
+ }
804
+ return {
805
+ response: result.text,
806
+ waitingForInput: false,
807
+ isComplete,
808
+ generatedReport,
809
+ };
810
+ },
811
+ });
812
+ //# sourceMappingURL=feedbackInterviewAgent.js.map