@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.
- package/README.md +346 -4
- package/dist/convex/_generated/api.d.ts +2310 -0
- package/dist/convex/_generated/api.d.ts.map +1 -0
- package/dist/convex/_generated/api.js +32 -0
- package/dist/convex/_generated/api.js.map +1 -0
- package/dist/convex/_generated/dataModel.d.ts +46 -0
- package/dist/convex/_generated/dataModel.d.ts.map +1 -0
- package/dist/convex/_generated/dataModel.js +11 -0
- package/dist/convex/_generated/dataModel.js.map +1 -0
- package/dist/convex/_generated/server.d.ts +121 -0
- package/dist/convex/_generated/server.d.ts.map +1 -0
- package/dist/convex/_generated/server.js +78 -0
- package/dist/convex/_generated/server.js.map +1 -0
- package/dist/convex/agents/bugReportAgent.d.ts +35 -9
- package/dist/convex/agents/bugReportAgent.d.ts.map +1 -1
- package/dist/convex/agents/bugReportAgent.js +61 -27
- package/dist/convex/agents/bugReportAgent.js.map +1 -1
- package/dist/convex/agents/feedbackAgent.d.ts +35 -9
- package/dist/convex/agents/feedbackAgent.d.ts.map +1 -1
- package/dist/convex/agents/feedbackAgent.js +61 -27
- package/dist/convex/agents/feedbackAgent.js.map +1 -1
- package/dist/convex/agents/feedbackInterviewAgent.d.ts +76 -0
- package/dist/convex/agents/feedbackInterviewAgent.d.ts.map +1 -0
- package/dist/convex/agents/feedbackInterviewAgent.js +812 -0
- package/dist/convex/agents/feedbackInterviewAgent.js.map +1 -0
- package/dist/convex/agents/index.d.ts +9 -0
- package/dist/convex/agents/index.d.ts.map +1 -0
- package/dist/convex/agents/index.js +13 -0
- package/dist/convex/agents/index.js.map +1 -0
- package/dist/convex/apiKeys.d.ts +56 -0
- package/dist/convex/apiKeys.d.ts.map +1 -0
- package/dist/convex/apiKeys.js +197 -0
- package/dist/convex/apiKeys.js.map +1 -0
- package/dist/convex/bugReports.d.ts +131 -11
- package/dist/convex/bugReports.d.ts.map +1 -1
- package/dist/convex/bugReports.js +138 -10
- package/dist/convex/bugReports.js.map +1 -1
- package/dist/convex/emails/bugReportEmails.d.ts +31 -2
- package/dist/convex/emails/bugReportEmails.d.ts.map +1 -1
- package/dist/convex/emails/bugReportEmails.js +6 -3
- package/dist/convex/emails/bugReportEmails.js.map +1 -1
- package/dist/convex/emails/feedbackEmails.d.ts +31 -2
- package/dist/convex/emails/feedbackEmails.d.ts.map +1 -1
- package/dist/convex/emails/feedbackEmails.js +6 -3
- package/dist/convex/emails/feedbackEmails.js.map +1 -1
- package/dist/convex/feedback.d.ts +132 -11
- package/dist/convex/feedback.d.ts.map +1 -1
- package/dist/convex/feedback.js +146 -9
- package/dist/convex/feedback.js.map +1 -1
- package/dist/convex/http.d.ts +39 -0
- package/dist/convex/http.d.ts.map +1 -0
- package/dist/convex/http.js +467 -0
- package/dist/convex/http.js.map +1 -0
- package/dist/convex/index.d.ts +8 -1
- package/dist/convex/index.d.ts.map +1 -1
- package/dist/convex/index.js +8 -1
- package/dist/convex/index.js.map +1 -1
- package/dist/convex/inputRequests.d.ts +118 -0
- package/dist/convex/inputRequests.d.ts.map +1 -0
- package/dist/convex/inputRequests.js +141 -0
- package/dist/convex/inputRequests.js.map +1 -0
- package/dist/convex/prompts.d.ts +110 -0
- package/dist/convex/prompts.d.ts.map +1 -0
- package/dist/convex/prompts.js +403 -0
- package/dist/convex/prompts.js.map +1 -0
- package/dist/convex/schema.d.ts +310 -54
- package/dist/convex/schema.d.ts.map +1 -1
- package/dist/convex/schema.js +120 -2
- package/dist/convex/schema.js.map +1 -1
- package/dist/convex/supportTeams.d.ts +69 -7
- package/dist/convex/supportTeams.d.ts.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -5
- package/src/convex/_generated/api.ts +1 -0
- package/src/convex/agents/feedbackInterviewAgent.ts +6 -12
- package/src/convex/apiKeys.test.ts +79 -0
- package/src/convex/apiKeys.ts +223 -0
- package/src/convex/bugReports.ts +126 -1
- package/src/convex/feedback.ts +134 -1
- package/src/convex/http.test.ts +76 -0
- package/src/convex/http.ts +630 -0
- package/src/convex/index.ts +11 -0
- package/src/convex/prompts.test.ts +185 -0
- package/src/convex/prompts.ts +605 -0
- package/src/convex/schema.ts +52 -2
- package/src/convex/ticketNumbers.ts +4 -0
- package/src/convex/tsconfig.json +24 -0
- package/src/index.ts +33 -1
- package/src/types.ts +38 -0
- package/dist/convex/convex.config.d.ts +0 -3
- package/dist/convex/convex.config.d.ts.map +0 -1
- package/dist/convex/convex.config.js +0 -6
- 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
|