diff --git a/packages/cli/src/services/insight/generators/DataProcessor.ts b/packages/cli/src/services/insight/generators/DataProcessor.ts index c5928ef54..5284dc110 100644 --- a/packages/cli/src/services/insight/generators/DataProcessor.ts +++ b/packages/cli/src/services/insight/generators/DataProcessor.ts @@ -17,116 +17,29 @@ import type { StreakData, SessionFacets, } from '../types/StaticInsightTypes.js'; +import type { + QualitativeInsights, + InsightImpressiveWorkflows, + InsightProjectAreas, + InsightFutureOpportunities, + InsightFrictionPoints, + InsightMemorableMoment, + InsightImprovements, + InsightInteractionStyle, + InsightAtAGlance, +} from '../types/QualitativeInsightTypes.js'; -// Prompt content from prompt.txt -const ANALYSIS_PROMPT = `Analyze this Qwen Code session and extract structured facets. - -CRITICAL GUIDELINES: - -1. **goal_categories**: Count ONLY what the USER explicitly asked for. - - DO NOT count Qwen's autonomous codebase exploration - - DO NOT count work Qwen decided to do on its own - - ONLY count when user says "can you...", "please...", "I need...", "let's..." - -2. **user_satisfaction_counts**: Base ONLY on explicit user signals. - - "Yay!", "great!", "perfect!" → happy - - "thanks", "looks good", "that works" → satisfied - - "ok, now let's..." (continuing without complaint) → likely_satisfied - - "that's not right", "try again" → dissatisfied - - "this is broken", "I give up" → frustrated - -3. **friction_counts**: Be specific about what went wrong. - - misunderstood_request: Qwen interpreted incorrectly - - wrong_approach: Right goal, wrong solution method - - buggy_code: Code didn't work correctly - - user_rejected_action: User said no/stop to a tool call - - excessive_changes: Over-engineered or changed too much - -4. If very short or just warmup, use warmup_minimal for goal_category`; - -const INSIGHT_SCHEMA = { - type: 'object', - properties: { - underlying_goal: { - type: 'string', - description: 'What the user fundamentally wanted to achieve', - }, - goal_categories: { - type: 'object', - additionalProperties: { type: 'number' }, - }, - outcome: { - type: 'string', - enum: [ - 'fully_achieved', - 'mostly_achieved', - 'partially_achieved', - 'not_achieved', - 'unclear_from_transcript', - ], - }, - user_satisfaction_counts: { - type: 'object', - additionalProperties: { type: 'number' }, - }, - Qwen_helpfulness: { - type: 'string', - enum: [ - 'unhelpful', - 'slightly_helpful', - 'moderately_helpful', - 'very_helpful', - 'essential', - ], - }, - session_type: { - type: 'string', - enum: [ - 'single_task', - 'multi_task', - 'iterative_refinement', - 'exploration', - 'quick_question', - ], - }, - friction_counts: { - type: 'object', - additionalProperties: { type: 'number' }, - }, - friction_detail: { - type: 'string', - description: 'One sentence describing friction or empty', - }, - primary_success: { - type: 'string', - enum: [ - 'none', - 'fast_accurate_search', - 'correct_code_edits', - 'good_explanations', - 'proactive_help', - 'multi_file_changes', - 'good_debugging', - ], - }, - brief_summary: { - type: 'string', - description: 'One sentence: what user wanted and whether they got it', - }, - }, - required: [ - 'underlying_goal', - 'goal_categories', - 'outcome', - 'user_satisfaction_counts', - 'Qwen_helpfulness', - 'session_type', - 'friction_counts', - 'friction_detail', - 'primary_success', - 'brief_summary', - ], -}; +import { + PROMPT_IMPRESSIVE_WORKFLOWS, + PROMPT_PROJECT_AREAS, + PROMPT_FUTURE_OPPORTUNITIES, + PROMPT_FRICTION_POINTS, + PROMPT_MEMORABLE_MOMENT, + PROMPT_IMPROVEMENTS, + PROMPT_INTERACTION_STYLE, + PROMPT_AT_A_GLANCE, + ANALYSIS_PROMPT, +} from '../prompts/InsightPrompts.js'; export class DataProcessor { constructor(private config: Config) {} @@ -159,8 +72,7 @@ export class DataProcessor { if ('text' in part && part.text) { output += `[Assistant]: ${part.text}\n`; } else if ('functionCall' in part) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const call = (part as any).functionCall; + const call = part.functionCall; if (call) { output += `[Tool: ${call.name}]\n`; } @@ -178,6 +90,90 @@ export class DataProcessor { ): Promise { if (records.length === 0) return null; + const INSIGHT_SCHEMA = { + type: 'object', + properties: { + underlying_goal: { + type: 'string', + description: 'What the user fundamentally wanted to achieve', + }, + goal_categories: { + type: 'object', + additionalProperties: { type: 'number' }, + }, + outcome: { + type: 'string', + enum: [ + 'fully_achieved', + 'mostly_achieved', + 'partially_achieved', + 'not_achieved', + 'unclear_from_transcript', + ], + }, + user_satisfaction_counts: { + type: 'object', + additionalProperties: { type: 'number' }, + }, + Qwen_helpfulness: { + type: 'string', + enum: [ + 'unhelpful', + 'slightly_helpful', + 'moderately_helpful', + 'very_helpful', + 'essential', + ], + }, + session_type: { + type: 'string', + enum: [ + 'single_task', + 'multi_task', + 'iterative_refinement', + 'exploration', + 'quick_question', + ], + }, + friction_counts: { + type: 'object', + additionalProperties: { type: 'number' }, + }, + friction_detail: { + type: 'string', + description: 'One sentence describing friction or empty', + }, + primary_success: { + type: 'string', + enum: [ + 'none', + 'fast_accurate_search', + 'correct_code_edits', + 'good_explanations', + 'proactive_help', + 'multi_file_changes', + 'good_debugging', + ], + }, + brief_summary: { + type: 'string', + description: 'One sentence: what user wanted and whether they got it', + }, + }, + required: [ + 'underlying_goal', + 'goal_categories', + 'outcome', + 'user_satisfaction_counts', + 'Qwen_helpfulness', + 'session_type', + 'friction_counts', + 'friction_detail', + 'primary_success', + 'brief_summary', + ], + }; + const sessionText = this.formatRecordsForAnalysis(records); const prompt = `${ANALYSIS_PROMPT}\n\nSESSION:\n${sessionText}`; @@ -367,14 +363,389 @@ export class DataProcessor { baseDir: string, facetsOutputDir?: string, ): Promise { - // Initialize data structures - const heatmap: HeatMapData = {}; - const tokenUsage: TokenUsageData = {}; - const activeHours: { [hour: number]: number } = {}; - const sessionStartTimes: { [sessionId: string]: Date } = {}; - const sessionEndTimes: { [sessionId: string]: Date } = {}; + const allChatFiles = await this.scanChatFiles(baseDir); - // Store all valid chat file paths for LLM analysis + const [metrics, facets] = await Promise.all([ + this.generateMetrics(allChatFiles), + this.generateFacets(allChatFiles, facetsOutputDir), + ]); + + const qualitative = await this.generateQualitativeInsights(metrics, facets); + + return { + ...metrics, + qualitative, + }; + } + + private async generateQualitativeInsights( + metrics: Omit, + facets: SessionFacets[], + ): Promise { + if (facets.length === 0) { + return undefined; + } + + console.log('Generating qualitative insights...'); + + const commonData = this.prepareCommonPromptData(metrics, facets); + + const generate = async ( + promptTemplate: string, + schema: Record, + ): Promise => { + const prompt = `${promptTemplate}\n\n${commonData}`; + try { + const result = await this.config.getBaseLlmClient().generateJson({ + model: this.config.getModel(), + contents: [{ role: 'user', parts: [{ text: prompt }] }], + schema, + abortSignal: AbortSignal.timeout(60000), + }); + return result as T; + } catch (error) { + console.error('Failed to generate insight:', error); + throw error; + } + }; + + // Schemas for each insight type + // We define simplified schemas here to guide the LLM. + // The types are already defined in QualitativeInsightTypes.ts + + // 1. Impressive Workflows + const schemaImpressiveWorkflows = { + type: 'object', + properties: { + intro: { type: 'string' }, + impressive_workflows: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['title', 'description'], + }, + }, + }, + required: ['intro', 'impressive_workflows'], + }; + + // 2. Project Areas + const schemaProjectAreas = { + type: 'object', + properties: { + areas: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + session_count: { type: 'number' }, + description: { type: 'string' }, + }, + required: ['name', 'session_count', 'description'], + }, + }, + }, + required: ['areas'], + }; + + // 3. Future Opportunities + const schemaFutureOpportunities = { + type: 'object', + properties: { + intro: { type: 'string' }, + opportunities: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + whats_possible: { type: 'string' }, + how_to_try: { type: 'string' }, + copyable_prompt: { type: 'string' }, + }, + required: [ + 'title', + 'whats_possible', + 'how_to_try', + 'copyable_prompt', + ], + }, + }, + }, + required: ['intro', 'opportunities'], + }; + + // 4. Friction Points + const schemaFrictionPoints = { + type: 'object', + properties: { + intro: { type: 'string' }, + categories: { + type: 'array', + items: { + type: 'object', + properties: { + category: { type: 'string' }, + description: { type: 'string' }, + examples: { type: 'array', items: { type: 'string' } }, + }, + required: ['category', 'description', 'examples'], + }, + }, + }, + required: ['intro', 'categories'], + }; + + // 5. Memorable Moment + const schemaMemorableMoment = { + type: 'object', + properties: { + headline: { type: 'string' }, + detail: { type: 'string' }, + }, + required: ['headline', 'detail'], + }; + + // 6. Improvements + const schemaImprovements = { + type: 'object', + properties: { + Qwen_md_additions: { + type: 'array', + items: { + type: 'object', + properties: { + addition: { type: 'string' }, + why: { type: 'string' }, + prompt_scaffold: { type: 'string' }, + }, + required: ['addition', 'why', 'prompt_scaffold'], + }, + }, + features_to_try: { + type: 'array', + items: { + type: 'object', + properties: { + feature: { type: 'string' }, + one_liner: { type: 'string' }, + why_for_you: { type: 'string' }, + example_code: { type: 'string' }, + }, + required: ['feature', 'one_liner', 'why_for_you', 'example_code'], + }, + }, + usage_patterns: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + suggestion: { type: 'string' }, + detail: { type: 'string' }, + copyable_prompt: { type: 'string' }, + }, + required: ['title', 'suggestion', 'detail', 'copyable_prompt'], + }, + }, + }, + required: ['Qwen_md_additions', 'features_to_try', 'usage_patterns'], + }; + + // 7. Interaction Style + const schemaInteractionStyle = { + type: 'object', + properties: { + narrative: { type: 'string' }, + key_pattern: { type: 'string' }, + }, + required: ['narrative', 'key_pattern'], + }; + + // 8. At A Glance + const schemaAtAGlance = { + type: 'object', + properties: { + whats_working: { type: 'string' }, + whats_hindering: { type: 'string' }, + quick_wins: { type: 'string' }, + ambitious_workflows: { type: 'string' }, + }, + required: [ + 'whats_working', + 'whats_hindering', + 'quick_wins', + 'ambitious_workflows', + ], + }; + + const limit = pLimit(4); + + try { + const [ + impressiveWorkflows, + projectAreas, + futureOpportunities, + frictionPoints, + memorableMoment, + improvements, + interactionStyle, + atAGlance, + ] = await Promise.all([ + limit(() => + generate( + PROMPT_IMPRESSIVE_WORKFLOWS, + schemaImpressiveWorkflows, + ), + ), + limit(() => + generate( + PROMPT_PROJECT_AREAS, + schemaProjectAreas, + ), + ), + limit(() => + generate( + PROMPT_FUTURE_OPPORTUNITIES, + schemaFutureOpportunities, + ), + ), + limit(() => + generate( + PROMPT_FRICTION_POINTS, + schemaFrictionPoints, + ), + ), + limit(() => + generate( + PROMPT_MEMORABLE_MOMENT, + schemaMemorableMoment, + ), + ), + limit(() => + generate( + PROMPT_IMPROVEMENTS, + schemaImprovements, + ), + ), + limit(() => + generate( + PROMPT_INTERACTION_STYLE, + schemaInteractionStyle, + ), + ), + limit(() => + generate(PROMPT_AT_A_GLANCE, schemaAtAGlance), + ), + ]); + + return { + impressiveWorkflows, + projectAreas, + futureOpportunities, + frictionPoints, + memorableMoment, + improvements, + interactionStyle, + atAGlance, + }; + } catch (e) { + console.error('Error generating qualitative insights:', e); + return undefined; + } + } + + private prepareCommonPromptData( + metrics: Omit, + facets: SessionFacets[], + ): string { + // 1. DATA section + const goalsAgg: Record = {}; + const outcomesAgg: Record = {}; + const satisfactionAgg: Record = {}; + const frictionAgg: Record = {}; + const successAgg: Record = {}; + + facets.forEach((facet) => { + // Aggregate goals + Object.entries(facet.goal_categories).forEach(([goal, count]) => { + goalsAgg[goal] = (goalsAgg[goal] || 0) + count; + }); + + // Aggregate outcomes + outcomesAgg[facet.outcome] = (outcomesAgg[facet.outcome] || 0) + 1; + + // Aggregate satisfaction + Object.entries(facet.user_satisfaction_counts).forEach(([sat, count]) => { + satisfactionAgg[sat] = (satisfactionAgg[sat] || 0) + count; + }); + + // Aggregate friction + Object.entries(facet.friction_counts).forEach(([fric, count]) => { + frictionAgg[fric] = (frictionAgg[fric] || 0) + count; + }); + + // Aggregate success (primary_success) + if (facet.primary_success && facet.primary_success !== 'none') { + successAgg[facet.primary_success] = + (successAgg[facet.primary_success] || 0) + 1; + } + }); + + const topGoals = Object.entries(goalsAgg) + .sort((a, b) => b[1] - a[1]) + .slice(0, 8); + + const dataObj = { + sessions: metrics.totalSessions || facets.length, + analyzed: facets.length, + date_range: { + start: Object.keys(metrics.heatmap).sort()[0] || 'N/A', + end: Object.keys(metrics.heatmap).sort().pop() || 'N/A', + }, + messages: metrics.totalMessages || 0, + hours: metrics.totalHours || 0, + commits: 0, // Not tracked yet + top_tools: metrics.topTools || [], + top_goals: topGoals, + outcomes: outcomesAgg, + satisfaction: satisfactionAgg, + friction: frictionAgg, + success: successAgg, + }; + + // 2. SESSION SUMMARIES section + const sessionSummaries = facets + .map((f) => `- ${f.brief_summary}`) + .join('\n'); + + // 3. FRICTION DETAILS section + const frictionDetails = facets + .filter((f) => f.friction_detail && f.friction_detail.trim().length > 0) + .map((f) => `- ${f.friction_detail}`) + .join('\n'); + + return `DATA: +${JSON.stringify(dataObj, null, 2)} + +SESSION SUMMARIES: +${sessionSummaries} + +FRICTION DETAILS: +${frictionDetails} + +USER INSTRUCTIONS TO Qwen: +None captured`; + } + + private async scanChatFiles( + baseDir: string, + ): Promise> { const allChatFiles: Array<{ path: string; mtime: number }> = []; try { @@ -390,11 +761,22 @@ export class DataProcessor { if (stats.isDirectory()) { const chatsDir = path.join(projectPath, 'chats'); - let chatFiles: string[] = []; try { // Get all chat files in the chats directory const files = await fs.readdir(chatsDir); - chatFiles = files.filter((file) => file.endsWith('.jsonl')); + const chatFiles = files.filter((file) => file.endsWith('.jsonl')); + + for (const file of chatFiles) { + const filePath = path.join(chatsDir, file); + + // Get file stats for sorting by recency + try { + const fileStats = await fs.stat(filePath); + allChatFiles.push({ path: filePath, mtime: fileStats.mtimeMs }); + } catch (e) { + console.error(`Failed to stat file ${filePath}:`, e); + } + } } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { console.log( @@ -404,68 +786,157 @@ export class DataProcessor { // Continue to next project if chats directory doesn't exist continue; } - - // Process each chat file in this project - for (const file of chatFiles) { - const filePath = path.join(chatsDir, file); - - // Get file stats for sorting by recency - try { - const fileStats = await fs.stat(filePath); - allChatFiles.push({ path: filePath, mtime: fileStats.mtimeMs }); - } catch (e) { - console.error(`Failed to stat file ${filePath}:`, e); - } - - const records = await readJsonlFile(filePath); - - // Process each record - for (const record of records) { - const timestamp = new Date(record.timestamp); - const dateKey = this.formatDate(timestamp); - const hour = timestamp.getHours(); - - // Update heatmap (count of interactions per day) - heatmap[dateKey] = (heatmap[dateKey] || 0) + 1; - - // Update active hours - activeHours[hour] = (activeHours[hour] || 0) + 1; - - // Update token usage - if (record.usageMetadata) { - const usage = tokenUsage[dateKey] || { - input: 0, - output: 0, - total: 0, - }; - - usage.input += record.usageMetadata.promptTokenCount || 0; - usage.output += record.usageMetadata.candidatesTokenCount || 0; - usage.total += record.usageMetadata.totalTokenCount || 0; - - tokenUsage[dateKey] = usage; - } - - // Track session times - if (!sessionStartTimes[record.sessionId]) { - sessionStartTimes[record.sessionId] = timestamp; - } - sessionEndTimes[record.sessionId] = timestamp; - } - } } } } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - // Base directory doesn't exist, return empty insights + // Base directory doesn't exist, return empty console.log(`Base directory does not exist: ${baseDir}`); } else { console.log(`Error reading base directory: ${error}`); } } + return allChatFiles; + } + + private async generateMetrics( + files: Array<{ path: string; mtime: number }>, + ): Promise> { + // Initialize data structures + const heatmap: HeatMapData = {}; + const tokenUsage: TokenUsageData = {}; + const activeHours: { [hour: number]: number } = {}; + const sessionStartTimes: { [sessionId: string]: Date } = {}; + const sessionEndTimes: { [sessionId: string]: Date } = {}; + let totalMessages = 0; + const toolUsage: Record = {}; + + for (const fileInfo of files) { + const records = await readJsonlFile(fileInfo.path); + totalMessages += records.length; + + // Process each record + for (const record of records) { + const timestamp = new Date(record.timestamp); + const dateKey = this.formatDate(timestamp); + const hour = timestamp.getHours(); + + // Update heatmap (count of interactions per day) + heatmap[dateKey] = (heatmap[dateKey] || 0) + 1; + + // Update active hours + activeHours[hour] = (activeHours[hour] || 0) + 1; + + // Update token usage + if (record.usageMetadata) { + const usage = tokenUsage[dateKey] || { + input: 0, + output: 0, + total: 0, + }; + + usage.input += record.usageMetadata.promptTokenCount || 0; + usage.output += record.usageMetadata.candidatesTokenCount || 0; + usage.total += record.usageMetadata.totalTokenCount || 0; + + tokenUsage[dateKey] = usage; + } + + // Track session times + if (!sessionStartTimes[record.sessionId]) { + sessionStartTimes[record.sessionId] = timestamp; + } + sessionEndTimes[record.sessionId] = timestamp; + + // Track tool usage + if (record.type === 'assistant' && record.message?.parts) { + for (const part of record.message.parts) { + if ('functionCall' in part) { + const name = part.functionCall!.name!; + toolUsage[name] = (toolUsage[name] || 0) + 1; + } + } + } + } + } + + // Calculate streak data + const streakData = this.calculateStreaks(Object.keys(heatmap)); + + // Calculate longest work session and total hours + let longestWorkDuration = 0; + let longestWorkDate: string | null = null; + let totalDurationMs = 0; + + const sessionIds = Object.keys(sessionStartTimes); + const totalSessions = sessionIds.length; + + for (const sessionId of sessionIds) { + const start = sessionStartTimes[sessionId]; + const end = sessionEndTimes[sessionId]; + const durationMs = end.getTime() - start.getTime(); + const durationMinutes = Math.round(durationMs / (1000 * 60)); + + totalDurationMs += durationMs; + + if (durationMinutes > longestWorkDuration) { + longestWorkDuration = durationMinutes; + longestWorkDate = this.formatDate(start); + } + } + + const totalHours = Math.round(totalDurationMs / (1000 * 60 * 60)); + + // Calculate latest active time + let latestActiveTime: string | null = null; + let latestTimestamp = new Date(0); + for (const dateStr in heatmap) { + const date = new Date(dateStr); + if (date > latestTimestamp) { + latestTimestamp = date; + latestActiveTime = date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + } + } + + // Calculate top tools + const topTools = Object.entries(toolUsage) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + + // Calculate achievements + const achievements = this.calculateAchievements( + activeHours, + heatmap, + tokenUsage, + ); + + return { + heatmap, + tokenUsage, + currentStreak: streakData.currentStreak, + longestStreak: streakData.longestStreak, + longestWorkDate, + longestWorkDuration, + activeHours, + latestActiveTime, + achievements, + totalSessions, + totalMessages, + totalHours, + topTools, + }; + } + + private async generateFacets( + allFiles: Array<{ path: string; mtime: number }>, + facetsOutputDir?: string, + ): Promise { // Sort files by recency (descending) and take top 50 - const recentFiles = allChatFiles + const recentFiles = [...allFiles] .sort((a, b) => b.mtime - a.mtime) .slice(0, 50); @@ -541,58 +1012,6 @@ export class DataProcessor { const facets = sessionFacetsWithNulls.filter( (f): f is SessionFacets => f !== null, ); - - // Calculate streak data - const streakData = this.calculateStreaks(Object.keys(heatmap)); - - // Calculate longest work session - let longestWorkDuration = 0; - let longestWorkDate: string | null = null; - for (const sessionId in sessionStartTimes) { - const start = sessionStartTimes[sessionId]; - const end = sessionEndTimes[sessionId]; - const durationMinutes = Math.round( - (end.getTime() - start.getTime()) / (1000 * 60), - ); - - if (durationMinutes > longestWorkDuration) { - longestWorkDuration = durationMinutes; - longestWorkDate = this.formatDate(start); - } - } - - // Calculate latest active time - let latestActiveTime: string | null = null; - let latestTimestamp = new Date(0); - for (const dateStr in heatmap) { - const date = new Date(dateStr); - if (date > latestTimestamp) { - latestTimestamp = date; - latestActiveTime = date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - } - } - - // Calculate achievements - const achievements = this.calculateAchievements( - activeHours, - heatmap, - tokenUsage, - ); - - return { - heatmap, - tokenUsage, - currentStreak: streakData.currentStreak, - longestStreak: streakData.longestStreak, - longestWorkDate, - longestWorkDuration, - activeHours, - latestActiveTime, - achievements, - facets, - }; + return facets; } } diff --git a/packages/cli/src/services/insight/prompts/InsightPrompts.ts b/packages/cli/src/services/insight/prompts/InsightPrompts.ts new file mode 100644 index 000000000..747783f98 --- /dev/null +++ b/packages/cli/src/services/insight/prompts/InsightPrompts.ts @@ -0,0 +1,152 @@ +export const ANALYSIS_PROMPT = `Analyze this Qwen Code session and extract structured facets. + +CRITICAL GUIDELINES: + +1. **goal_categories**: Count ONLY what the USER explicitly asked for. + - DO NOT count Qwen's autonomous codebase exploration + - DO NOT count work Qwen decided to do on its own + - ONLY count when user says "can you...", "please...", "I need...", "let's..." + +2. **user_satisfaction_counts**: Base ONLY on explicit user signals. + - "Yay!", "great!", "perfect!" → happy + - "thanks", "looks good", "that works" → satisfied + - "ok, now let's..." (continuing without complaint) → likely_satisfied + - "that's not right", "try again" → dissatisfied + - "this is broken", "I give up" → frustrated + +3. **friction_counts**: Be specific about what went wrong. + - misunderstood_request: Qwen interpreted incorrectly + - wrong_approach: Right goal, wrong solution method + - buggy_code: Code didn't work correctly + - user_rejected_action: User said no/stop to a tool call + - excessive_changes: Over-engineered or changed too much + +4. If very short or just warmup, use warmup_minimal for goal_category`; + +export const PROMPT_IMPRESSIVE_WORKFLOWS = `Analyze this Qwen Code usage data and identify what's working well for this user. Use second person ("you"). + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "intro": "1 sentence of context", + "impressive_workflows": [ + {"title": "Short title (3-6 words)", "description": "2-3 sentences describing the impressive workflow or approach. Use 'you' not 'the user'."} + ] +} + +Include 3 impressive workflows.`; + +export const PROMPT_PROJECT_AREAS = `Analyze this Qwen Code usage data and identify project areas. + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "areas": [ + {"name": "Area name", "session_count": N, "description": "2-3 sentences about what was worked on and how Qwen Code was used."} + ] +} + +Include 4-5 areas. Skip internal CC operations.`; + +export const PROMPT_FUTURE_OPPORTUNITIES = `Analyze this Qwen Code usage data and identify future opportunities. + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "intro": "1 sentence about evolving AI-assisted development", + "opportunities": [ + {"title": "Short title (4-8 words)", "whats_possible": "2-3 ambitious sentences about autonomous workflows", "how_to_try": "1-2 sentences mentioning relevant tooling", "copyable_prompt": "Detailed prompt to try"} + ] +} + +Include 3 opportunities. Think BIG - autonomous workflows, parallel agents, iterating against tests.`; + +export const PROMPT_FRICTION_POINTS = `Analyze this Qwen Code usage data and identify friction points for this user. Use second person ("you"). + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "intro": "1 sentence summarizing friction patterns", + "categories": [ + {"category": "Concrete category name", "description": "1-2 sentences explaining this category and what could be done differently. Use 'you' not 'the user'.", "examples": ["Specific example with consequence", "Another example"]} + ] +} + +Include 3 friction categories with 2 examples each.`; + +export const PROMPT_MEMORABLE_MOMENT = `Analyze this Qwen Code usage data and find a memorable moment. + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "headline": "A memorable QUALITATIVE moment from the transcripts - not a statistic. Something human, funny, or surprising.", + "detail": "Brief context about when/where this happened" +} + +Find something genuinely interesting or amusing from the session summaries.`; + +export const PROMPT_IMPROVEMENTS = `Analyze this Qwen Code usage data and suggest improvements. + +## CC FEATURES REFERENCE (pick from these for features_to_try): +1. **MCP Servers**: Connect Qwen to external tools, databases, and APIs via Model Context Protocol. + - How to use: Run \`Qwen mcp add -- \` + - Good for: database queries, Slack integration, GitHub issue lookup, connecting to internal APIs + +2. **Custom Skills**: Reusable prompts you define as markdown files that run with a single /command. + - How to use: Create \`.Qwen/skills/commit/SKILL.md\` with instructions. Then type \`/commit\` to run it. + - Good for: repetitive workflows - /commit, /review, /test, /deploy, /pr, or complex multi-step workflows + +3. **Hooks**: Shell commands that auto-run at specific lifecycle events. + - How to use: Add to \`.Qwen/settings.json\` under "hooks" key. + - Good for: auto-formatting code, running type checks, enforcing conventions + +4. **Headless Mode**: Run Qwen non-interactively from scripts and CI/CD. + - How to use: \`Qwen -p "fix lint errors" --allowedTools "Edit,Read,Bash"\` + - Good for: CI/CD integration, batch code fixes, automated reviews + +5. **Task Agents**: Qwen spawns focused sub-agents for complex exploration or parallel work. + - How to use: Qwen auto-invokes when helpful, or ask "use an agent to explore X" + - Good for: codebase exploration, understanding complex systems + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "Qwen_md_additions": [ + {"addition": "A specific line or block to add to Qwen.md based on workflow patterns. E.g., 'Always run tests after modifying auth-related files'", "why": "1 sentence explaining why this would help based on actual sessions", "prompt_scaffold": "Instructions for where to add this in Qwen.md. E.g., 'Add under ## Testing section'"} + ], + "features_to_try": [ + {"feature": "Feature name from CC FEATURES REFERENCE above", "one_liner": "What it does", "why_for_you": "Why this would help YOU based on your sessions", "example_code": "Actual command or config to copy"} + ], + "usage_patterns": [ + {"title": "Short title", "suggestion": "1-2 sentence summary", "detail": "3-4 sentences explaining how this applies to YOUR work", "copyable_prompt": "A specific prompt to copy and try"} + ] +} + +IMPORTANT for Qwen_md_additions: PRIORITIZE instructions that appear MULTIPLE TIMES in the user data. If user told Qwen the same thing in 2+ sessions (e.g., 'always run tests', 'use TypeScript'), that's a PRIME candidate - they shouldn't have to repeat themselves. + +IMPORTANT for features_to_try: Pick 2-3 from the CC FEATURES REFERENCE above. Include 2-3 items for each category.`; + +export const PROMPT_INTERACTION_STYLE = `Analyze this Qwen Code usage data and describe the user's interaction style. + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "narrative": "2-3 paragraphs analyzing HOW the user interacts with Qwen Code. Use second person 'you'. Describe patterns: iterate quickly vs detailed upfront specs? Interrupt often or let Qwen run? Include specific examples. Use **bold** for key insights.", + "key_pattern": "One sentence summary of most distinctive interaction style" +} +`; + +export const PROMPT_AT_A_GLANCE = `You're writing an "At a Glance" summary for a Qwen Code usage insights report for Qwen Code users. The goal is to help them understand their usage and improve how they can use Qwen better, especially as models improve. + +Use this 4-part structure: + +1. **What's working** - What is the user's unique style of interacting with Qwen and what are some impactful things they've done? You can include one or two details, but keep it high level since things might not be fresh in the user's memory. Don't be fluffy or overly complimentary. Also, don't focus on the tool calls they use. + +2. **What's hindering you** - Split into (a) Qwen's fault (misunderstandings, wrong approaches, bugs) and (b) user-side friction (not providing enough context, environment issues -- ideally more general than just one project). Be honest but constructive. + +3. **Quick wins to try** - Specific Qwen Code features they could try from the examples below, or a workflow technique if you think it's really compelling. (Avoid stuff like "Ask Qwen to confirm before taking actions" or "Type out more context up front" which are less compelling.) + +4. **Ambitious workflows for better models** - As we move to much more capable models over the next 3-6 months, what should they prepare for? What workflows that seem impossible now will become possible? Draw from the appropriate section below. + +Keep each section to 2-3 not-too-long sentences. Don't overwhelm the user. Don't mention specific numerical stats or underlined_categories from the session data below. Use a coaching tone. + +RESPOND WITH ONLY A VALID JSON OBJECT: +{ + "whats_working": "(refer to instructions above)", + "whats_hindering": "(refer to instructions above)", + "quick_wins": "(refer to instructions above)", + "ambitious_workflows": "(refer to instructions above)" +}`; diff --git a/packages/cli/src/services/insight/types/QualitativeInsightTypes.ts b/packages/cli/src/services/insight/types/QualitativeInsightTypes.ts new file mode 100644 index 000000000..fc9546b98 --- /dev/null +++ b/packages/cli/src/services/insight/types/QualitativeInsightTypes.ts @@ -0,0 +1,82 @@ +export interface InsightImpressiveWorkflows { + intro: string; + impressive_workflows: Array<{ + title: string; + description: string; + }>; +} + +export interface InsightProjectAreas { + areas: Array<{ + name: string; + session_count: number; + description: string; + }>; +} + +export interface InsightFutureOpportunities { + intro: string; + opportunities: Array<{ + title: string; + whats_possible: string; + how_to_try: string; + copyable_prompt: string; + }>; +} + +export interface InsightFrictionPoints { + intro: string; + categories: Array<{ + category: string; + description: string; + examples: string[]; + }>; +} + +export interface InsightMemorableMoment { + headline: string; + detail: string; +} + +export interface InsightImprovements { + Qwen_md_additions: Array<{ + addition: string; + why: string; + prompt_scaffold: string; + }>; + features_to_try: Array<{ + feature: string; + one_liner: string; + why_for_you: string; + example_code: string; + }>; + usage_patterns: Array<{ + title: string; + suggestion: string; + detail: string; + copyable_prompt: string; + }>; +} + +export interface InsightInteractionStyle { + narrative: string; + key_pattern: string; +} + +export interface InsightAtAGlance { + whats_working: string; + whats_hindering: string; + quick_wins: string; + ambitious_workflows: string; +} + +export interface QualitativeInsights { + impressiveWorkflows: InsightImpressiveWorkflows; + projectAreas: InsightProjectAreas; + futureOpportunities: InsightFutureOpportunities; + frictionPoints: InsightFrictionPoints; + memorableMoment: InsightMemorableMoment; + improvements: InsightImprovements; + interactionStyle: InsightInteractionStyle; + atAGlance: InsightAtAGlance; +} diff --git a/packages/cli/src/services/insight/types/StaticInsightTypes.ts b/packages/cli/src/services/insight/types/StaticInsightTypes.ts index d1d132e66..f926a8d83 100644 --- a/packages/cli/src/services/insight/types/StaticInsightTypes.ts +++ b/packages/cli/src/services/insight/types/StaticInsightTypes.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { QualitativeInsights } from './QualitativeInsightTypes.js'; + export interface UsageMetadata { input: number; output: number; @@ -34,7 +36,11 @@ export interface InsightData { activeHours: { [hour: number]: number }; latestActiveTime: string | null; achievements: AchievementData[]; - facets?: SessionFacets[]; + totalSessions?: number; + totalMessages?: number; + totalHours?: number; + topTools?: Array<[string, number]>; + qualitative?: QualitativeInsights; } export interface StreakData {