Skip to content

Commit 9665760

Browse files
authored
fix(insight): handle individual LLM failures in qualitative insights (#2341) (#2361)
Previously, `generateQualitativeInsights` used `Promise.all` with a `generate` helper that re-threw errors. A single LLM call failure (timeout, rate limit, JSON parse error) caused the entire `qualitative` object to become `undefined`, hiding all detailed report sections. Now individual `generate` calls catch errors and return `undefined` instead of throwing. The `QualitativeInsights` interface fields are made optional so partial results render correctly — each React section component already guards against missing data with `if (!field) return null`. Made-with: Cursor
1 parent 1359563 commit 9665760

File tree

3 files changed

+107
-10
lines changed

3 files changed

+107
-10
lines changed

packages/cli/src/services/insight/generators/DataProcessor.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ vi.mock('@qwen-code/qwen-code-core', async () => {
2424
info: vi.fn(),
2525
error: vi.fn(),
2626
warn: vi.fn(),
27+
debug: vi.fn(),
2728
})),
2829
};
2930
});
@@ -1137,6 +1138,102 @@ describe('DataProcessor', () => {
11371138
});
11381139
});
11391140

1141+
describe('generateQualitativeInsights', () => {
1142+
const mockMetrics = {
1143+
totalSessions: 5,
1144+
totalMessages: 50,
1145+
totalHours: 2,
1146+
heatmap: { '2025-01-15': 3 },
1147+
topTools: [['read_file', 10]] as Array<[string, number]>,
1148+
activeDays: 1,
1149+
activeHours: { '10': 5 },
1150+
totalLinesAdded: 100,
1151+
totalLinesRemoved: 50,
1152+
totalFiles: 10,
1153+
streak: { currentStreak: 1, longestStreak: 1, dates: [] },
1154+
} as unknown as Omit<InsightData, 'facets' | 'qualitative'>;
1155+
1156+
const mockFacets: SessionFacets[] = [
1157+
{
1158+
session_id: 'test-1',
1159+
underlying_goal: 'Fix bug',
1160+
goal_categories: { debugging: 1 },
1161+
outcome: 'fully_achieved',
1162+
user_satisfaction_counts: { satisfied: 1 },
1163+
Qwen_helpfulness: 'very_helpful',
1164+
session_type: 'single_task',
1165+
friction_counts: {},
1166+
friction_detail: '',
1167+
primary_success: 'correct_code_edits',
1168+
brief_summary: 'Fixed a bug',
1169+
},
1170+
];
1171+
1172+
it('should return partial qualitative data when some LLM calls fail', async () => {
1173+
let callIndex = 0;
1174+
mockGenerateJson.mockImplementation(() => {
1175+
callIndex++;
1176+
if (callIndex % 2 === 0) {
1177+
return Promise.reject(new Error('LLM timeout'));
1178+
}
1179+
return Promise.resolve({ intro: 'test', areas: [], opportunities: [] });
1180+
});
1181+
1182+
const result = await (
1183+
dataProcessor as unknown as {
1184+
generateQualitativeInsights(
1185+
metrics: Omit<InsightData, 'facets' | 'qualitative'>,
1186+
facets: SessionFacets[],
1187+
): Promise<
1188+
| import('../types/QualitativeInsightTypes.js').QualitativeInsights
1189+
| undefined
1190+
>;
1191+
}
1192+
).generateQualitativeInsights(mockMetrics, mockFacets);
1193+
1194+
expect(result).toBeDefined();
1195+
expect(result!.impressiveWorkflows).toBeDefined();
1196+
expect(result!.projectAreas).toBeUndefined();
1197+
expect(result!.futureOpportunities).toBeDefined();
1198+
expect(result!.frictionPoints).toBeUndefined();
1199+
});
1200+
1201+
it('should return undefined when facets are empty', async () => {
1202+
const result = await (
1203+
dataProcessor as unknown as {
1204+
generateQualitativeInsights(
1205+
metrics: Omit<InsightData, 'facets' | 'qualitative'>,
1206+
facets: SessionFacets[],
1207+
): Promise<
1208+
| import('../types/QualitativeInsightTypes.js').QualitativeInsights
1209+
| undefined
1210+
>;
1211+
}
1212+
).generateQualitativeInsights(mockMetrics, []);
1213+
1214+
expect(result).toBeUndefined();
1215+
});
1216+
1217+
it('should return full qualitative data when all LLM calls succeed', async () => {
1218+
mockGenerateJson.mockResolvedValue({ intro: 'test', areas: [] });
1219+
1220+
const result = await (
1221+
dataProcessor as unknown as {
1222+
generateQualitativeInsights(
1223+
metrics: Omit<InsightData, 'facets' | 'qualitative'>,
1224+
facets: SessionFacets[],
1225+
): Promise<
1226+
| import('../types/QualitativeInsightTypes.js').QualitativeInsights
1227+
| undefined
1228+
>;
1229+
}
1230+
).generateQualitativeInsights(mockMetrics, mockFacets);
1231+
1232+
expect(result).toBeDefined();
1233+
expect(mockGenerateJson).toHaveBeenCalledTimes(8);
1234+
});
1235+
});
1236+
11401237
describe('generateFacets', () => {
11411238
it('should skip non-conversational sessions', async () => {
11421239
const userOnlyRecords: ChatRecord[] = [

packages/cli/src/services/insight/generators/DataProcessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ export class DataProcessor {
388388
const generate = async <T>(
389389
promptTemplate: string,
390390
schema: Record<string, unknown>,
391-
): Promise<T> => {
391+
): Promise<T | undefined> => {
392392
const prompt = `${promptTemplate}\n\n${commonData}`;
393393
try {
394394
const result = await this.config.getBaseLlmClient().generateJson({
@@ -400,7 +400,7 @@ export class DataProcessor {
400400
return result as T;
401401
} catch (error) {
402402
logger.error('Failed to generate insight:', error);
403-
throw error;
403+
return undefined;
404404
}
405405
};
406406

packages/cli/src/services/insight/types/QualitativeInsightTypes.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ export interface InsightAtAGlance {
7171
}
7272

7373
export interface QualitativeInsights {
74-
impressiveWorkflows: InsightImpressiveWorkflows;
75-
projectAreas: InsightProjectAreas;
76-
futureOpportunities: InsightFutureOpportunities;
77-
frictionPoints: InsightFrictionPoints;
78-
memorableMoment: InsightMemorableMoment;
79-
improvements: InsightImprovements;
80-
interactionStyle: InsightInteractionStyle;
81-
atAGlance: InsightAtAGlance;
74+
impressiveWorkflows?: InsightImpressiveWorkflows;
75+
projectAreas?: InsightProjectAreas;
76+
futureOpportunities?: InsightFutureOpportunities;
77+
frictionPoints?: InsightFrictionPoints;
78+
memorableMoment?: InsightMemorableMoment;
79+
improvements?: InsightImprovements;
80+
interactionStyle?: InsightInteractionStyle;
81+
atAGlance?: InsightAtAGlance;
8282
}

0 commit comments

Comments
 (0)