@fatagnus/convex-feedback 0.2.1 → 0.2.3
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/package.json
CHANGED
|
@@ -19,14 +19,17 @@ import { action, query, mutation, internalMutation } from "../_generated/server"
|
|
|
19
19
|
import type { Id } from "../_generated/dataModel";
|
|
20
20
|
import type { BugSeverity, FeedbackType, FeedbackPriority } from "../schema";
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
22
|
+
// Helper to create OpenRouter provider with a specific API key
|
|
23
|
+
// This is created dynamically because Convex components don't inherit parent env vars
|
|
24
|
+
function createOpenRouterProvider(apiKey: string) {
|
|
25
|
+
return createOpenAICompatible({
|
|
26
|
+
name: "openrouter",
|
|
27
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${apiKey}`,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
30
33
|
|
|
31
34
|
// Agent name constant
|
|
32
35
|
const AGENT_NAME = "Feedback Interview Agent";
|
|
@@ -186,101 +189,6 @@ const generateFeedback = createTool({
|
|
|
186
189
|
},
|
|
187
190
|
});
|
|
188
191
|
|
|
189
|
-
// ============================================================================
|
|
190
|
-
// Agent Definition
|
|
191
|
-
// ============================================================================
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Bug Report Interview Agent
|
|
195
|
-
*/
|
|
196
|
-
export const bugInterviewAgent = new Agent(components.agent, {
|
|
197
|
-
name: "Bug Report Interview Agent",
|
|
198
|
-
languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"),
|
|
199
|
-
|
|
200
|
-
instructions: `You are a helpful assistant that interviews users to gather detailed information about bugs they've encountered.
|
|
201
|
-
|
|
202
|
-
Your goal is to understand the bug thoroughly and generate a high-quality bug report.
|
|
203
|
-
|
|
204
|
-
## Interview Flow
|
|
205
|
-
|
|
206
|
-
1. **Greet briefly** and ask what went wrong (the bug they encountered).
|
|
207
|
-
|
|
208
|
-
2. **Understand the bug**:
|
|
209
|
-
- What happened? (the actual behavior)
|
|
210
|
-
- What did they expect to happen? (expected behavior)
|
|
211
|
-
- How often does this occur? (always, sometimes, once)
|
|
212
|
-
|
|
213
|
-
3. **Get reproduction info**:
|
|
214
|
-
- What steps led to this bug?
|
|
215
|
-
- Can they reliably reproduce it?
|
|
216
|
-
|
|
217
|
-
4. **Assess impact**:
|
|
218
|
-
- How does this affect their work?
|
|
219
|
-
- How urgent is this for them?
|
|
220
|
-
|
|
221
|
-
5. **Generate the report** using the generateBugReport tool when you have enough information.
|
|
222
|
-
|
|
223
|
-
## Guidelines
|
|
224
|
-
- Be conversational but efficient - aim for 3-5 exchanges
|
|
225
|
-
- Use choice inputs when there are clear options
|
|
226
|
-
- Use text inputs for open-ended questions
|
|
227
|
-
- Don't ask for technical details the user might not know
|
|
228
|
-
- Focus on understanding the user experience
|
|
229
|
-
- Generate the report as soon as you have enough info`,
|
|
230
|
-
|
|
231
|
-
tools: {
|
|
232
|
-
requestUserInput,
|
|
233
|
-
generateBugReport,
|
|
234
|
-
},
|
|
235
|
-
|
|
236
|
-
maxSteps: 30,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Feedback Interview Agent
|
|
241
|
-
*/
|
|
242
|
-
export const feedbackInterviewAgent = new Agent(components.agent, {
|
|
243
|
-
name: "Feedback Interview Agent",
|
|
244
|
-
languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"),
|
|
245
|
-
|
|
246
|
-
instructions: `You are a helpful assistant that interviews users to gather detailed feedback and suggestions.
|
|
247
|
-
|
|
248
|
-
Your goal is to understand their idea thoroughly and generate well-structured feedback.
|
|
249
|
-
|
|
250
|
-
## Interview Flow
|
|
251
|
-
|
|
252
|
-
1. **Greet briefly** and ask about their idea or suggestion.
|
|
253
|
-
|
|
254
|
-
2. **Understand the need**:
|
|
255
|
-
- What problem does this solve?
|
|
256
|
-
- What's their current workaround (if any)?
|
|
257
|
-
|
|
258
|
-
3. **Clarify the request**:
|
|
259
|
-
- What would the ideal solution look like?
|
|
260
|
-
- Is this a new feature, change to existing, or general feedback?
|
|
261
|
-
|
|
262
|
-
4. **Assess importance**:
|
|
263
|
-
- How important is this to them?
|
|
264
|
-
- Who else might benefit?
|
|
265
|
-
|
|
266
|
-
5. **Generate the feedback** using the generateFeedback tool when you have enough information.
|
|
267
|
-
|
|
268
|
-
## Guidelines
|
|
269
|
-
- Be conversational but efficient - aim for 3-5 exchanges
|
|
270
|
-
- Use choice inputs when there are clear options
|
|
271
|
-
- Use text inputs for open-ended questions
|
|
272
|
-
- Help users articulate their ideas clearly
|
|
273
|
-
- Focus on understanding the value and impact
|
|
274
|
-
- Generate the feedback as soon as you have enough info`,
|
|
275
|
-
|
|
276
|
-
tools: {
|
|
277
|
-
requestUserInput,
|
|
278
|
-
generateFeedback,
|
|
279
|
-
},
|
|
280
|
-
|
|
281
|
-
maxSteps: 30,
|
|
282
|
-
});
|
|
283
|
-
|
|
284
192
|
// ============================================================================
|
|
285
193
|
// Internal Mutations
|
|
286
194
|
// ============================================================================
|
|
@@ -570,6 +478,7 @@ Your goal is to understand their idea thoroughly and generate well-structured fe
|
|
|
570
478
|
*/
|
|
571
479
|
export const startBugInterview = action({
|
|
572
480
|
args: {
|
|
481
|
+
openRouterApiKey: v.string(),
|
|
573
482
|
reporterType: v.union(v.literal("staff"), v.literal("customer")),
|
|
574
483
|
reporterId: v.string(),
|
|
575
484
|
reporterEmail: v.string(),
|
|
@@ -605,10 +514,8 @@ export const startBugInterview = action({
|
|
|
605
514
|
})),
|
|
606
515
|
}),
|
|
607
516
|
handler: async (ctx, args) => {
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
throw new Error("OPENROUTER_API_KEY not configured. Interview mode requires AI.");
|
|
611
|
-
}
|
|
517
|
+
// Create OpenRouter provider with the passed API key
|
|
518
|
+
const openrouter = createOpenRouterProvider(args.openRouterApiKey);
|
|
612
519
|
|
|
613
520
|
// Create a thread for the interview
|
|
614
521
|
const threadId = await createThread(ctx, components.agent, {
|
|
@@ -642,14 +549,30 @@ export const startBugInterview = action({
|
|
|
642
549
|
});
|
|
643
550
|
|
|
644
551
|
// Start the interview
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
552
|
+
let result;
|
|
553
|
+
try {
|
|
554
|
+
result = await dynamicAgent.generateText(
|
|
555
|
+
ctx,
|
|
556
|
+
{
|
|
557
|
+
threadId,
|
|
558
|
+
customCtx: { sessionId },
|
|
559
|
+
},
|
|
560
|
+
{ prompt: "Start the bug report interview. Greet the user briefly and ask what bug or issue they encountered." }
|
|
561
|
+
);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
// Provide more descriptive error messages based on the error type
|
|
564
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
565
|
+
if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
|
|
566
|
+
throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY.");
|
|
567
|
+
} else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) {
|
|
568
|
+
throw new Error("AI service rate limited. Please try again in a few moments.");
|
|
569
|
+
} else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) {
|
|
570
|
+
throw new Error("AI service temporarily unavailable. Please try again later.");
|
|
571
|
+
} else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
|
|
572
|
+
throw new Error("AI service request timed out. Please try again.");
|
|
573
|
+
}
|
|
574
|
+
throw new Error(`Failed to start interview: ${errorMessage}`);
|
|
575
|
+
}
|
|
653
576
|
|
|
654
577
|
// Check for pending input request
|
|
655
578
|
const pendingRequest = await ctx.runQuery(
|
|
@@ -686,6 +609,7 @@ export const startBugInterview = action({
|
|
|
686
609
|
*/
|
|
687
610
|
export const startFeedbackInterview = action({
|
|
688
611
|
args: {
|
|
612
|
+
openRouterApiKey: v.string(),
|
|
689
613
|
reporterType: v.union(v.literal("staff"), v.literal("customer")),
|
|
690
614
|
reporterId: v.string(),
|
|
691
615
|
reporterEmail: v.string(),
|
|
@@ -721,10 +645,8 @@ export const startFeedbackInterview = action({
|
|
|
721
645
|
})),
|
|
722
646
|
}),
|
|
723
647
|
handler: async (ctx, args) => {
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
throw new Error("OPENROUTER_API_KEY not configured. Interview mode requires AI.");
|
|
727
|
-
}
|
|
648
|
+
// Create OpenRouter provider with the passed API key
|
|
649
|
+
const openrouter = createOpenRouterProvider(args.openRouterApiKey);
|
|
728
650
|
|
|
729
651
|
// Create a thread for the interview
|
|
730
652
|
const threadId = await createThread(ctx, components.agent, {
|
|
@@ -758,14 +680,30 @@ export const startFeedbackInterview = action({
|
|
|
758
680
|
});
|
|
759
681
|
|
|
760
682
|
// Start the interview
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
683
|
+
let result;
|
|
684
|
+
try {
|
|
685
|
+
result = await dynamicAgent.generateText(
|
|
686
|
+
ctx,
|
|
687
|
+
{
|
|
688
|
+
threadId,
|
|
689
|
+
customCtx: { sessionId },
|
|
690
|
+
},
|
|
691
|
+
{ prompt: "Start the feedback interview. Greet the user briefly and ask about their idea or suggestion." }
|
|
692
|
+
);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
// Provide more descriptive error messages based on the error type
|
|
695
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
696
|
+
if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
|
|
697
|
+
throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY.");
|
|
698
|
+
} else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) {
|
|
699
|
+
throw new Error("AI service rate limited. Please try again in a few moments.");
|
|
700
|
+
} else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) {
|
|
701
|
+
throw new Error("AI service temporarily unavailable. Please try again later.");
|
|
702
|
+
} else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
|
|
703
|
+
throw new Error("AI service request timed out. Please try again.");
|
|
704
|
+
}
|
|
705
|
+
throw new Error(`Failed to start interview: ${errorMessage}`);
|
|
706
|
+
}
|
|
769
707
|
|
|
770
708
|
// Check for pending input request
|
|
771
709
|
const pendingRequest = await ctx.runQuery(
|
|
@@ -802,6 +740,7 @@ export const startFeedbackInterview = action({
|
|
|
802
740
|
*/
|
|
803
741
|
export const continueInterview = action({
|
|
804
742
|
args: {
|
|
743
|
+
openRouterApiKey: v.string(),
|
|
805
744
|
threadId: v.string(),
|
|
806
745
|
sessionId: v.string(),
|
|
807
746
|
requestId: v.id("feedbackInputRequests"),
|
|
@@ -859,6 +798,9 @@ export const continueInterview = action({
|
|
|
859
798
|
})),
|
|
860
799
|
}),
|
|
861
800
|
handler: async (ctx, args) => {
|
|
801
|
+
// Create OpenRouter provider with the passed API key
|
|
802
|
+
const openrouter = createOpenRouterProvider(args.openRouterApiKey);
|
|
803
|
+
|
|
862
804
|
// Submit the response
|
|
863
805
|
await ctx.runMutation(
|
|
864
806
|
api.inputRequests.submitResponse,
|
|
@@ -887,18 +829,34 @@ export const continueInterview = action({
|
|
|
887
829
|
});
|
|
888
830
|
|
|
889
831
|
// Continue the agent
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
832
|
+
let result;
|
|
833
|
+
try {
|
|
834
|
+
result = await dynamicAgent.generateText(
|
|
835
|
+
ctx,
|
|
836
|
+
{
|
|
837
|
+
threadId: args.threadId,
|
|
838
|
+
customCtx: { sessionId: args.sessionId },
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
prompt: `The user responded: ${args.response}
|
|
898
842
|
|
|
899
843
|
Please continue the interview based on their response.`,
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
// Provide more descriptive error messages based on the error type
|
|
848
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
849
|
+
if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
|
|
850
|
+
throw new Error("AI service authentication failed. Please check your OPENROUTER_API_KEY.");
|
|
851
|
+
} else if (errorMessage.includes("429") || errorMessage.includes("rate limit")) {
|
|
852
|
+
throw new Error("AI service rate limited. Please try again in a few moments.");
|
|
853
|
+
} else if (errorMessage.includes("503") || errorMessage.includes("unavailable")) {
|
|
854
|
+
throw new Error("AI service temporarily unavailable. Please try again later.");
|
|
855
|
+
} else if (errorMessage.includes("timeout") || errorMessage.includes("ETIMEDOUT")) {
|
|
856
|
+
throw new Error("AI service request timed out. Please try again.");
|
|
900
857
|
}
|
|
901
|
-
|
|
858
|
+
throw new Error(`Failed to continue interview: ${errorMessage}`);
|
|
859
|
+
}
|
|
902
860
|
|
|
903
861
|
// Check if interview is complete (session has generated report)
|
|
904
862
|
const session = await ctx.runQuery(api.agents.feedbackInterviewAgent.getSessionByThread, {
|
|
@@ -752,11 +752,12 @@ export function BugReportButton({
|
|
|
752
752
|
};
|
|
753
753
|
|
|
754
754
|
// Auto-start interview when interview mode is selected and no interview is active
|
|
755
|
+
// Important: Check for error state to prevent infinite retry loop on failure
|
|
755
756
|
useEffect(() => {
|
|
756
|
-
if (opened && inputMode === 'interview' && enableInterview && !interviewState.threadId && !interviewState.isThinking) {
|
|
757
|
+
if (opened && inputMode === 'interview' && enableInterview && !interviewState.threadId && !interviewState.isThinking && !interviewState.error) {
|
|
757
758
|
handleStartInterview();
|
|
758
759
|
}
|
|
759
|
-
}, [opened, inputMode, enableInterview, interviewState.threadId, interviewState.isThinking, handleStartInterview]);
|
|
760
|
+
}, [opened, inputMode, enableInterview, interviewState.threadId, interviewState.isThinking, interviewState.error, handleStartInterview]);
|
|
760
761
|
|
|
761
762
|
return (
|
|
762
763
|
<>
|
|
@@ -844,9 +845,21 @@ export function BugReportButton({
|
|
|
844
845
|
{interviewState.error && (
|
|
845
846
|
<Alert icon={<IconAlertCircle size={16} />} color="red" title="Interview Error">
|
|
846
847
|
{interviewState.error}
|
|
847
|
-
<
|
|
848
|
-
|
|
849
|
-
|
|
848
|
+
<Group gap="xs" mt="xs">
|
|
849
|
+
<Button
|
|
850
|
+
variant="subtle"
|
|
851
|
+
size="xs"
|
|
852
|
+
onClick={() => {
|
|
853
|
+
// Clear error and retry
|
|
854
|
+
setInterviewState(prev => ({ ...prev, error: null }));
|
|
855
|
+
}}
|
|
856
|
+
>
|
|
857
|
+
Try Again
|
|
858
|
+
</Button>
|
|
859
|
+
<Button variant="subtle" size="xs" onClick={() => setInputMode('form')}>
|
|
860
|
+
Switch to Form
|
|
861
|
+
</Button>
|
|
862
|
+
</Group>
|
|
850
863
|
</Alert>
|
|
851
864
|
)}
|
|
852
865
|
|