From f07fba7c0039b6b4153211dc7d4f9e0b53523cf3 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Thu, 23 Apr 2026 16:59:33 +0530 Subject: [PATCH 1/4] Add suggested customer response, similar issue detection, and workaround suggestions to AI triage - Added dedicated LLM call to generate copy-paste-ready customer responses for all issue categories - Added similar/duplicate issue detection: searches recent open + closed issues via GitHub API, uses LLM to identify duplicates and recently fixed related issues - Added workaround generation for BUG/BREAK_FIX: suggests practical workarounds with code snippets and downgrade guidance - Added 'Similar Issues & Recent Fixes', 'Workarounds', and 'Suggested Response to Customer' sections in Teams notification card - Updated test-triage-local.js to match new workflow logic - Similar issues and workaround context fed into customer response for richer replies --- .github/workflows/issue-notify.yml | 77 ++++++++- .github/workflows/issue-triage.yml | 228 +++++++++++++++++++++++- test-triage-local.js | 269 ++++++++++++++++++++++++++++- 3 files changed, 568 insertions(+), 6 deletions(-) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 29f5b1864..d1195edb7 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -32,6 +32,18 @@ on: required: false type: string default: '' + suggested_response: + required: false + type: string + default: '' + similar_issues: + required: false + type: string + default: '' + workaround: + required: false + type: string + default: '' issue_number: required: true type: string @@ -69,6 +81,9 @@ jobs: INPUT_SUMMARY: ${{ inputs.summary_for_maintainers }} INPUT_CODE_ANALYSIS: ${{ inputs.code_analysis }} INPUT_ENGINEER_GUIDANCE: ${{ inputs.engineer_guidance }} + INPUT_SUGGESTED_RESPONSE: ${{ inputs.suggested_response }} + INPUT_SIMILAR_ISSUES: ${{ inputs.similar_issues }} + INPUT_WORKAROUND: ${{ inputs.workaround }} INPUT_JUSTIFICATION: ${{ inputs.justification }} TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} run: | @@ -142,7 +157,6 @@ jobs: else empty end), (if .implementation_approach then "Implementation Approach: " + (.implementation_approach | @html) else empty end), (if .risks_and_tradeoffs then "Risks & Tradeoffs: " + (.risks_and_tradeoffs | @html) else empty end), - (if .suggested_response then "Suggested Response to User:
" + (.suggested_response | @html) else empty end), (if .related_considerations and (.related_considerations | length) > 0 then "Related Considerations:
" + ([.related_considerations | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + (.value | @html)] | join("
")) else empty end) @@ -152,6 +166,50 @@ jobs: ENGINEER_GUIDANCE="" fi + # Format suggested customer response + SUGGESTED_RESPONSE_RAW="$INPUT_SUGGESTED_RESPONSE" + if [ -n "$SUGGESTED_RESPONSE_RAW" ]; then + SUGGESTED_RESPONSE=$(echo "$SUGGESTED_RESPONSE_RAW" | sed 's/&/\&/g; s//\>/g; s/$/\/g') + else + SUGGESTED_RESPONSE="" + fi + + # Parse and format similar issues analysis + SIMILAR_ISSUES_RAW="$INPUT_SIMILAR_ISSUES" + if [ -n "$SIMILAR_ISSUES_RAW" ]; then + SIMILAR_ISSUES=$(echo "$SIMILAR_ISSUES_RAW" | jq -r ' + [ + (if .summary then "Summary: " + (.summary | @html) else empty end), + (if .duplicate_issues and (.duplicate_issues | length) > 0 + then "Similar/Duplicate Issues:
" + ([.duplicate_issues[] | "  • #" + (.issue_number | tostring) + " " + (.title | @html) + " [" + .state + "] — " + (.similarity | @html) + ": " + (.explanation | @html)] | join("
")) + else empty end), + (if .recently_fixed and (.recently_fixed | length) > 0 + then "Recently Fixed:
" + ([.recently_fixed[] | "  • #" + (.issue_number | tostring) + " " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("
")) + else empty end) + ] | join("

") + ' 2>/dev/null || echo "$SIMILAR_ISSUES_RAW" | sed 's/&/\&/g; s//\>/g') + else + SIMILAR_ISSUES="" + fi + + # Parse and format workaround analysis + WORKAROUND_RAW="$INPUT_WORKAROUND" + if [ -n "$WORKAROUND_RAW" ]; then + WORKAROUND=$(echo "$WORKAROUND_RAW" | jq -r ' + [ + (if .summary then "Summary: " + (.summary | @html) else empty end), + (if .has_workaround and .workarounds and (.workarounds | length) > 0 + then "Workarounds:
" + ([.workarounds | to_entries[] | "  " + ((.key + 1) | tostring) + ". " + (.value.description | @html) + " [" + .value.confidence + " confidence]
    Limitations: " + (.value.limitations | @html) + (if .value.code_snippet and (.value.code_snippet | length) > 0 then "
    Code: " + (.value.code_snippet | @html) + "" else "" end)] | join("
")) + else empty end), + (if .can_downgrade == true and .downgrade_version + then "⬇️ Safe to downgrade to: " + (.downgrade_version | @html) + else empty end) + ] | join("

") + ' 2>/dev/null || echo "$WORKAROUND_RAW" | sed 's/&/\&/g; s//\>/g') + else + WORKAROUND="" + fi + # Set severity color indicator case "$SEVERITY" in critical) SEV_INDICATOR="🔴" ;; @@ -176,6 +234,9 @@ jobs: --arg summary "$INPUT_SUMMARY" \ --arg code_analysis "$CODE_ANALYSIS" \ --arg engineer_guidance "$ENGINEER_GUIDANCE" \ + --arg suggested_response "$SUGGESTED_RESPONSE" \ + --arg similar_issues "$SIMILAR_ISSUES" \ + --arg workaround "$WORKAROUND" \ --arg justification "$INPUT_JUSTIFICATION" \ --arg action "$ACTION" \ --arg repo_url "https://github.com/microsoft/mssql-python" \ @@ -198,10 +259,24 @@ jobs: "

" + ($summary | @html) + "

" + "

🔍 Code Analysis

" + "

" + $code_analysis + "

" + + (if $similar_issues != "" then + "

🔄 Similar Issues & Recent Fixes

" + + "

" + $similar_issues + "

" + else "" end) + + (if $workaround != "" then + "

🛠️ Workarounds

" + + "

" + $workaround + "

" + else "" end) + (if $engineer_guidance != "" then "

💡 Engineer Guidance

" + "

" + $engineer_guidance + "

" else "" end) + + (if $suggested_response != "" then + "
" + + "

✉️ Suggested Response to Customer

" + + "

Copy-paste or edit the response below and post it on the issue:

" + + "
" + $suggested_response + "
" + else "" end) + "
" + "

Action Required: " + $action + "

" + "

⚠️ AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.

" + diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index d4e959952..e7d181610 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -228,7 +228,6 @@ jobs: "implementation_approach": "", "effort_estimate": "small|medium|large|epic", "risks_and_tradeoffs": "", - "suggested_response": "", "related_considerations": [""] } @@ -243,9 +242,227 @@ jobs: } } + // --- Search for similar/duplicate issues and recent fixes --- + console.log('Searching for similar issues and recent fixes...'); + let similarIssuesAnalysis = ''; + + try { + // Fetch recent open issues (last 30) + const openIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 30, + sort: 'created', + direction: 'desc' + }); + + // Fetch recently closed issues (last 30) + const closedIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'closed', + per_page: 30, + sort: 'updated', + direction: 'desc' + }); + + const allIssues = [...openIssues.data, ...closedIssues.data] + .filter(i => i.number !== issueNumber && !i.pull_request) + .map(i => ({ + number: i.number, + title: i.title, + state: i.state, + labels: i.labels.map(l => l.name).join(', '), + created_at: i.created_at, + closed_at: i.closed_at, + body_preview: (i.body || '').slice(0, 300) + })); + + console.log(`Found ${allIssues.length} issues to compare against`); + + const similarPrompt = ` + You are an expert triage system for the mssql-python repository. + A new issue has been filed. Your job is to check if any similar or duplicate issues + already exist (open or recently closed/fixed). + + NEW ISSUE: + Title: ${issueTitle} + Body: ${issueBody.slice(0, 2000)} + Keywords: ${analysis.keywords.join(', ')} + + EXISTING ISSUES (recent open + recently closed): + ${JSON.stringify(allIssues, null, 2).slice(0, 6000)} + + Analyze and respond in JSON: + { + "has_duplicates": true/false, + "has_similar": true/false, + "duplicate_issues": [ + { + "issue_number": , + "title": "", + "state": "open|closed", + "similarity": "duplicate|highly_similar|related", + "explanation": "<why this is a duplicate/similar>" + } + ], + "recently_fixed": [ + { + "issue_number": <number>, + "title": "<title>", + "closed_at": "<date>", + "relevance": "<how this fix relates to the new issue>" + } + ], + "summary": "<1-3 sentence summary: are there duplicates? was this recently fixed? should the new issue be closed as duplicate?>" + } + + IMPORTANT: + - Only flag as duplicate if the core problem is truly the same, not just topically related + - For recently_fixed, only include issues that were closed AND directly address the same problem + - If no duplicates or similar issues exist, set has_duplicates and has_similar to false with empty arrays + `; + + const similarResult = await callGitHubModels(similarPrompt); + similarIssuesAnalysis = similarResult; + const parsed = JSON.parse(similarResult); + console.log(`Similar issues check: duplicates=${parsed.has_duplicates}, similar=${parsed.has_similar}`); + } catch (e) { + console.log(`Similar issues search failed: ${e.message}`); + similarIssuesAnalysis = ''; + } + + // --- Generate workaround suggestions (BUG/BREAK_FIX only) --- + let workaroundAnalysis = ''; + + if (['BUG', 'BREAK_FIX'].includes(analysis.category)) { + console.log('Generating workaround suggestions...'); + + const workaroundContext = codeAnalysis + ? `Code Analysis:\n${codeAnalysis}` + : ''; + + const workaroundPrompt = ` + You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. + A user has reported a ${analysis.category === 'BREAK_FIX' ? 'regression/break-fix' : 'bug'}. + Your job is to suggest practical workarounds the user can apply RIGHT NOW while the team works on a proper fix. + + Issue Title: ${issueTitle} + Issue Body: + ${issueBody.slice(0, 2000)} + + ${workaroundContext} + ${codeContext} + + Provide workaround suggestions in JSON: + { + "has_workaround": true/false, + "workarounds": [ + { + "description": "<clear description of the workaround>", + "code_snippet": "<Python code snippet showing the workaround, if applicable>", + "limitations": "<any limitations or caveats of this workaround>", + "confidence": "high|medium|low" + } + ], + "can_downgrade": true/false, + "downgrade_version": "<safe version to downgrade to, if applicable>", + "summary": "<1-2 sentence summary of available workarounds>" + } + + IMPORTANT: + - Only suggest workarounds you are confident about based on the code and issue description + - Workarounds should be practical and safe for production use + - If no reliable workaround exists, set has_workaround to false + - For BREAK_FIX issues, always consider if downgrading to a previous version is viable + - Code snippets should be complete and copy-pasteable + `; + + try { + workaroundAnalysis = await callGitHubModels(workaroundPrompt); + const parsed = JSON.parse(workaroundAnalysis); + console.log(`Workaround analysis: has_workaround=${parsed.has_workaround}`); + } catch (e) { + console.log(`Workaround generation failed: ${e.message}`); + workaroundAnalysis = ''; + } + } + // NO labels modified on the issue — label info sent to Teams only // NO comment posted to the issue + // --- Generate suggested customer response (all categories) --- + console.log('Generating suggested customer response...'); + let suggestedResponse = ''; + + const analysisContext = codeAnalysis + ? `Code Analysis:\n${codeAnalysis}` + : engineerGuidance + ? `Engineer Guidance:\n${engineerGuidance}` + : ''; + + const workaroundContext = workaroundAnalysis + ? `Workaround Analysis:\n${workaroundAnalysis}` + : ''; + + const similarContext = similarIssuesAnalysis + ? `Similar Issues Analysis:\n${similarIssuesAnalysis}` + : ''; + + const responsePrompt = ` + You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. + A customer filed a GitHub issue and you need to craft a helpful, professional, and empathetic response + that an engineer can copy-paste (or lightly edit) and post on the issue. + + Issue Category: ${analysis.category} + Severity: ${analysis.severity} + Issue Title: ${issueTitle} + Issue Author: @${issueAuthor} + Issue Body: + ${issueBody.slice(0, 3000)} + + ${analysisContext} + + ${workaroundContext} + + ${similarContext} + + Write a suggested response following these guidelines: + - Address the author by their GitHub username (@username) + - Thank them for filing the issue + - Acknowledge the specific problem or request they described + - For BUG/BREAK_FIX: Let them know the team is investigating; ask for OS, Python version, + mssql-python version, SQL Server version, and a minimal repro script if not already provided; + if workarounds are available, mention them briefly + - For FEATURE_REQUEST: Acknowledge the value of the request; mention the team will evaluate it; + ask for use-case details or code examples showing desired behavior if not provided + - For DISCUSSION: Provide helpful guidance or clarification based on the analysis; + point to relevant docs or code if applicable + - If similar or duplicate issues were found, mention them (e.g., "This looks related to #123") + - Always ask for a minimal reproduction script/code snippet if the user hasn't provided one + - Keep the tone warm, professional, and collaborative + - Use Markdown formatting suitable for GitHub comments + - Do NOT promise timelines or specific fixes + - Do NOT reveal internal triage details or AI involvement + - Keep it concise (under 200 words) + + Respond in JSON: + { + "suggested_response": "<the full Markdown response ready to post on GitHub>" + } + `; + + try { + const responseResult = await callGitHubModels(responsePrompt); + const parsed = JSON.parse(responseResult); + suggestedResponse = parsed.suggested_response || ''; + console.log('Suggested customer response generated'); + } catch (e) { + console.log(`Suggested response generation failed: ${e.message}`); + suggestedResponse = ''; + } + // --- Store outputs --- core.setOutput('category', analysis.category); core.setOutput('confidence', analysis.confidence.toString()); @@ -256,6 +473,9 @@ jobs: core.setOutput('keywords', analysis.keywords.join(', ')); core.setOutput('code_analysis', codeAnalysis); core.setOutput('engineer_guidance', engineerGuidance); + core.setOutput('suggested_response', suggestedResponse); + core.setOutput('similar_issues', similarIssuesAnalysis); + core.setOutput('workaround', workaroundAnalysis); core.setOutput('issue_number', issueNumber.toString()); core.setOutput('issue_title', issueTitle); core.setOutput('issue_url', issue.html_url); @@ -271,6 +491,9 @@ jobs: keywords: ${{ steps.triage.outputs.keywords }} code_analysis: ${{ steps.triage.outputs.code_analysis }} engineer_guidance: ${{ steps.triage.outputs.engineer_guidance }} + suggested_response: ${{ steps.triage.outputs.suggested_response }} + similar_issues: ${{ steps.triage.outputs.similar_issues }} + workaround: ${{ steps.triage.outputs.workaround }} issue_number: ${{ steps.triage.outputs.issue_number }} issue_title: ${{ steps.triage.outputs.issue_title }} issue_url: ${{ steps.triage.outputs.issue_url }} @@ -289,6 +512,9 @@ jobs: keywords: ${{ needs.triage.outputs.keywords }} code_analysis: ${{ needs.triage.outputs.code_analysis }} engineer_guidance: ${{ needs.triage.outputs.engineer_guidance }} + suggested_response: ${{ needs.triage.outputs.suggested_response }} + similar_issues: ${{ needs.triage.outputs.similar_issues }} + workaround: ${{ needs.triage.outputs.workaround }} issue_number: ${{ needs.triage.outputs.issue_number }} issue_title: ${{ needs.triage.outputs.issue_title }} issue_url: ${{ needs.triage.outputs.issue_url }} diff --git a/test-triage-local.js b/test-triage-local.js index f67eb142d..6603ff6e5 100644 --- a/test-triage-local.js +++ b/test-triage-local.js @@ -94,7 +94,7 @@ async function fetchFileContent(filePath) { } // --- Helper: Send Teams notification --- -async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, issue) { +async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, suggestedResponse, similarIssuesAnalysis, workaroundAnalysis, issue) { const category = analysis.category; const severity = analysis.severity; @@ -171,7 +171,6 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, i } if (parsed.implementation_approach) parts.push(`<b>Implementation Approach:</b> ${escVal(parsed.implementation_approach)}`); if (parsed.risks_and_tradeoffs) parts.push(`<b>Risks & Tradeoffs:</b> ${escVal(parsed.risks_and_tradeoffs)}`); - if (parsed.suggested_response) parts.push(`<b>Suggested Response to User:</b><br>${escVal(parsed.suggested_response)}`); if (parsed.related_considerations && parsed.related_considerations.length > 0) { parts.push(`<b>Related Considerations:</b><br>${parsed.related_considerations.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("<br>")}`); } @@ -184,6 +183,52 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, i } } + // Format similar issues analysis + let similarIssuesText = ""; + if (similarIssuesAnalysis) { + try { + const parsed = JSON.parse(similarIssuesAnalysis); + const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + const parts = []; + if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); + if (parsed.duplicate_issues && parsed.duplicate_issues.length > 0) { + parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.map(d => + `  • <a href="https://github.com/microsoft/mssql-python/issues/${d.issue_number}">#${d.issue_number}</a> ${escVal(d.title)} [${d.state}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}` + ).join("<br>")}`); + } + if (parsed.recently_fixed && parsed.recently_fixed.length > 0) { + parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.map(f => + `  • <a href="https://github.com/microsoft/mssql-python/issues/${f.issue_number}">#${f.issue_number}</a> ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}` + ).join("<br>")}`); + } + similarIssuesText = parts.join("<br><br>"); + } catch (e) { + similarIssuesText = esc(similarIssuesAnalysis); + } + } + + // Format workaround analysis + let workaroundText = ""; + if (workaroundAnalysis) { + try { + const parsed = JSON.parse(workaroundAnalysis); + const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + const parts = []; + if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); + if (parsed.has_workaround && parsed.workarounds && parsed.workarounds.length > 0) { + parts.push(`<b>Workarounds:</b><br>${parsed.workarounds.map((w, i) => + `  ${i + 1}. <b>${escVal(w.description)}</b> [${w.confidence} confidence]<br>    Limitations: ${escVal(w.limitations)}${w.code_snippet ? `<br>    Code: <code>${escVal(w.code_snippet)}</code>` : ''}` + ).join("<br>")}`); + } + if (parsed.can_downgrade && parsed.downgrade_version) { + parts.push(`⬇️ <b>Safe to downgrade to:</b> ${escVal(parsed.downgrade_version)}`); + } + workaroundText = parts.join("<br><br>"); + } catch (e) { + workaroundText = esc(workaroundAnalysis); + } + } + const htmlMessage = [ `<h2>${emoji} mssql-python Issue Triage</h2>`, `<p><b>${esc(categoryDisplay)}</b>  |  `, @@ -201,8 +246,16 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, i `<p>${esc(analysis.summary_for_maintainers)}</p>`, `<h3>🔍 Code Analysis</h3>`, `<p>${codeAnalysisText}</p>`, + similarIssuesText ? `<h3>🔄 Similar Issues & Recent Fixes</h3>` : '', + similarIssuesText ? `<p>${similarIssuesText}</p>` : '', + workaroundText ? `<h3>🛠️ Workarounds</h3>` : '', + workaroundText ? `<p>${workaroundText}</p>` : '', engineerGuidanceText ? `<h3>💡 Engineer Guidance</h3>` : '', engineerGuidanceText ? `<p>${engineerGuidanceText}</p>` : '', + suggestedResponse ? `<hr>` : '', + suggestedResponse ? `<h3>✉️ Suggested Response to Customer</h3>` : '', + suggestedResponse ? `<p><i>Copy-paste or edit the response below and post it on the issue:</i></p>` : '', + suggestedResponse ? `<blockquote>${esc(suggestedResponse)}</blockquote>` : '', `<hr>`, `<p>⚡ <b>Action Required:</b> ${esc(action)}</p>`, `<p><i>⚠️ AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.</i></p>`, @@ -393,7 +446,6 @@ Respond in JSON: "implementation_approach": "<concrete implementation steps referencing specific functions/lines from the code — ONLY if issue_identified is true, otherwise empty string>", "effort_estimate": "small|medium|large|epic", "risks_and_tradeoffs": "<potential risks, backward compatibility concerns, or tradeoffs — ONLY if issue_identified is true, otherwise empty string>", - "suggested_response": "<a draft response the engineer could post on the issue. Always ask the user to share a minimal repro or code snippet that demonstrates the issue or desired behavior, if they haven't already provided one.>", "related_considerations": ["<other things the team should think about — ONLY if issue_identified is true, otherwise empty array>"] } @@ -415,10 +467,219 @@ IMPORTANT: If your technical_assessment does not identify any actual issue or ga } } + // --- Search for similar/duplicate issues and recent fixes --- + console.log(`\n🔄 Searching for similar issues and recent fixes...`); + let similarIssuesAnalysis = ''; + + try { + const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "triage-test" }; + if (process.env.GH_TOKEN) headers["Authorization"] = `token ${process.env.GH_TOKEN}`; + + const openRes = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=open&per_page=30&sort=created&direction=desc`, { headers }); + const openIssues = await openRes.json(); + + const closedRes = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=closed&per_page=30&sort=updated&direction=desc`, { headers }); + const closedIssues = await closedRes.json(); + + const allIssues = [...openIssues, ...closedIssues] + .filter(i => i.number !== issueNumber && !i.pull_request) + .map(i => ({ + number: i.number, + title: i.title, + state: i.state, + labels: i.labels.map(l => l.name).join(', '), + created_at: i.created_at, + closed_at: i.closed_at, + body_preview: (i.body || '').slice(0, 300) + })); + + console.log(` Found ${allIssues.length} issues to compare against`); + + const similarPrompt = ` +You are an expert triage system for the mssql-python repository. +A new issue has been filed. Your job is to check if any similar or duplicate issues +already exist (open or recently closed/fixed). + +NEW ISSUE: +Title: ${issue.title} +Body: ${(issue.body || '').slice(0, 2000)} +Keywords: ${analysis.keywords.join(', ')} + +EXISTING ISSUES (recent open + recently closed): +${JSON.stringify(allIssues, null, 2).slice(0, 6000)} + +Analyze and respond in JSON: +{ + "has_duplicates": true/false, + "has_similar": true/false, + "duplicate_issues": [ + { + "issue_number": <number>, + "title": "<title>", + "state": "open|closed", + "similarity": "duplicate|highly_similar|related", + "explanation": "<why this is a duplicate/similar>" + } + ], + "recently_fixed": [ + { + "issue_number": <number>, + "title": "<title>", + "closed_at": "<date>", + "relevance": "<how this fix relates to the new issue>" + } + ], + "summary": "<1-3 sentence summary: are there duplicates? was this recently fixed? should the new issue be closed as duplicate?>" +} + +IMPORTANT: +- Only flag as duplicate if the core problem is truly the same, not just topically related +- For recently_fixed, only include issues that were closed AND directly address the same problem +- If no duplicates or similar issues exist, set has_duplicates and has_similar to false with empty arrays +`; + + similarIssuesAnalysis = await callGitHubModels(similarPrompt); + const parsed = JSON.parse(similarIssuesAnalysis); + console.log(` Duplicates: ${parsed.has_duplicates}, Similar: ${parsed.has_similar}`); + console.log(` Summary: ${parsed.summary}`); + } catch (e) { + console.log(` ⚠️ Similar issues search failed: ${e.message}`); + } + + // --- Generate workaround suggestions (BUG/BREAK_FIX only) --- + let workaroundAnalysis = ''; + + if (["BUG", "BREAK_FIX"].includes(analysis.category)) { + console.log(`\n🛠️ Generating workaround suggestions...`); + + const workaroundCtx = codeAnalysis ? `Code Analysis:\n${codeAnalysis}` : ''; + + const workaroundPrompt = ` +You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. +A user has reported a ${analysis.category === "BREAK_FIX" ? "regression/break-fix" : "bug"}. +Your job is to suggest practical workarounds the user can apply RIGHT NOW while the team works on a proper fix. + +Issue Title: ${issue.title} +Issue Body: +${(issue.body || "").slice(0, 2000)} + +${workaroundCtx} +${codeContext} + +Provide workaround suggestions in JSON: +{ + "has_workaround": true/false, + "workarounds": [ + { + "description": "<clear description of the workaround>", + "code_snippet": "<Python code snippet showing the workaround, if applicable>", + "limitations": "<any limitations or caveats of this workaround>", + "confidence": "high|medium|low" + } + ], + "can_downgrade": true/false, + "downgrade_version": "<safe version to downgrade to, if applicable>", + "summary": "<1-2 sentence summary of available workarounds>" +} + +IMPORTANT: +- Only suggest workarounds you are confident about based on the code and issue description +- Workarounds should be practical and safe for production use +- If no reliable workaround exists, set has_workaround to false +- For BREAK_FIX issues, always consider if downgrading to a previous version is viable +- Code snippets should be complete and copy-pasteable +`; + + try { + workaroundAnalysis = await callGitHubModels(workaroundPrompt); + const parsed = JSON.parse(workaroundAnalysis); + console.log(` Has workaround: ${parsed.has_workaround}`); + console.log(` Summary: ${parsed.summary}`); + if (parsed.workarounds && parsed.workarounds.length > 0) { + for (const w of parsed.workarounds) { + console.log(` • ${w.description} [${w.confidence}]`); + } + } + } catch (e) { + console.log(` ⚠️ Workaround generation failed: ${e.message}`); + } + } + + // --- Generate suggested customer response (all categories) --- + console.log(`\n✉️ Generating suggested customer response...`); + let suggestedResponse = ''; + + const analysisContext = codeAnalysis + ? `Code Analysis:\n${codeAnalysis}` + : engineerGuidance + ? `Engineer Guidance:\n${engineerGuidance}` + : ''; + + const workaroundResponseCtx = workaroundAnalysis + ? `Workaround Analysis:\n${workaroundAnalysis}` + : ''; + + const similarResponseCtx = similarIssuesAnalysis + ? `Similar Issues Analysis:\n${similarIssuesAnalysis}` + : ''; + + const responsePrompt = ` +You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. +A customer filed a GitHub issue and you need to craft a helpful, professional, and empathetic response +that an engineer can copy-paste (or lightly edit) and post on the issue. + +Issue Category: ${analysis.category} +Severity: ${analysis.severity} +Issue Title: ${issue.title} +Issue Author: @${issue.user.login} +Issue Body: +${(issue.body || '').slice(0, 3000)} + +${analysisContext} + +${workaroundResponseCtx} + +${similarResponseCtx} + +Write a suggested response following these guidelines: +- Address the author by their GitHub username (@username) +- Thank them for filing the issue +- Acknowledge the specific problem or request they described +- For BUG/BREAK_FIX: Let them know the team is investigating; ask for OS, Python version, + mssql-python version, SQL Server version, and a minimal repro script if not already provided; + if workarounds are available, mention them briefly +- For FEATURE_REQUEST: Acknowledge the value of the request; mention the team will evaluate it; + ask for use-case details or code examples showing desired behavior if not provided +- For DISCUSSION: Provide helpful guidance or clarification based on the analysis; + point to relevant docs or code if applicable +- If similar or duplicate issues were found, mention them (e.g., "This looks related to #123") +- Always ask for a minimal reproduction script/code snippet if the user hasn't provided one +- Keep the tone warm, professional, and collaborative +- Use Markdown formatting suitable for GitHub comments +- Do NOT promise timelines or specific fixes +- Do NOT reveal internal triage details or AI involvement +- Keep it concise (under 200 words) + +Respond in JSON: +{ + "suggested_response": "<the full Markdown response ready to post on GitHub>" +} +`; + + try { + const responseResult = await callGitHubModels(responsePrompt); + const parsed = JSON.parse(responseResult); + suggestedResponse = parsed.suggested_response || ''; + console.log(` ✅ Suggested response generated`); + console.log(`\n✉️ Suggested Response:\n${suggestedResponse}`); + } catch (e) { + console.log(` ⚠️ Suggested response generation failed: ${e.message}`); + } + // --- Send Teams notification --- console.log(`\n📤 Sending Teams notification...`); try { - const status = await sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, issue); + const status = await sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, suggestedResponse, similarIssuesAnalysis, workaroundAnalysis, issue); console.log(` ✅ Teams notification sent (HTTP ${status})`); } catch (e) { console.error(` ❌ Teams notification failed: ${e.message}`); From 334ea7b2403b3f177fdc8b3dbda0d3de1948b03f Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai <sumitsar@microsoft.com> Date: Thu, 23 Apr 2026 17:32:57 +0530 Subject: [PATCH 2/4] Fix: HTML-escape all LLM-provided fields and validate issue_number in hrefs - Escape .state and .confidence via @html (jq) / escVal() (JS) instead of raw interpolation - Validate .issue_number as numeric-only before using in href attributes (jq: test, JS: regex filter + Number()) - Entries with non-numeric issue_number are silently dropped from the output - Applied consistently in issue-notify.yml and test-triage-local.js --- .github/workflows/issue-notify.yml | 6 +++--- test-triage-local.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index d1195edb7..5167636d8 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -181,10 +181,10 @@ jobs: [ (if .summary then "<b>Summary:</b> " + (.summary | @html) else empty end), (if .duplicate_issues and (.duplicate_issues | length) > 0 - then "<b>Similar/Duplicate Issues:</b><br>" + ([.duplicate_issues[] | "  • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " [" + .state + "] — <i>" + (.similarity | @html) + ":</i> " + (.explanation | @html)] | join("<br>")) + then "<b>Similar/Duplicate Issues:</b><br>" + ([.duplicate_issues[] | select(.issue_number | tostring | test("^[0-9]+$")) | "  • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " [" + (.state | @html) + "] — <i>" + (.similarity | @html) + ":</i> " + (.explanation | @html)] | join("<br>")) else empty end), (if .recently_fixed and (.recently_fixed | length) > 0 - then "<b>Recently Fixed:</b><br>" + ([.recently_fixed[] | "  • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("<br>")) + then "<b>Recently Fixed:</b><br>" + ([.recently_fixed[] | select(.issue_number | tostring | test("^[0-9]+$")) | "  • <a href=\"https://github.com/microsoft/mssql-python/issues/" + (.issue_number | tostring) + "\">#" + (.issue_number | tostring) + "</a> " + (.title | @html) + " (closed " + (.closed_at | @html) + ") — " + (.relevance | @html)] | join("<br>")) else empty end) ] | join("<br><br>") ' 2>/dev/null || echo "$SIMILAR_ISSUES_RAW" | sed 's/&/\&/g; s/</\</g; s/>/\>/g') @@ -199,7 +199,7 @@ jobs: [ (if .summary then "<b>Summary:</b> " + (.summary | @html) else empty end), (if .has_workaround and .workarounds and (.workarounds | length) > 0 - then "<b>Workarounds:</b><br>" + ([.workarounds | to_entries[] | "  " + ((.key + 1) | tostring) + ". <b>" + (.value.description | @html) + "</b> [" + .value.confidence + " confidence]<br>    Limitations: " + (.value.limitations | @html) + (if .value.code_snippet and (.value.code_snippet | length) > 0 then "<br>    Code: <code>" + (.value.code_snippet | @html) + "</code>" else "" end)] | join("<br>")) + then "<b>Workarounds:</b><br>" + ([.workarounds | to_entries[] | "  " + ((.key + 1) | tostring) + ". <b>" + (.value.description | @html) + "</b> [" + (.value.confidence | @html) + " confidence]<br>    Limitations: " + (.value.limitations | @html) + (if .value.code_snippet and (.value.code_snippet | length) > 0 then "<br>    Code: <code>" + (.value.code_snippet | @html) + "</code>" else "" end)] | join("<br>")) else empty end), (if .can_downgrade == true and .downgrade_version then "⬇️ <b>Safe to downgrade to:</b> " + (.downgrade_version | @html) diff --git a/test-triage-local.js b/test-triage-local.js index 6603ff6e5..0ca55b200 100644 --- a/test-triage-local.js +++ b/test-triage-local.js @@ -192,13 +192,13 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, s const parts = []; if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); if (parsed.duplicate_issues && parsed.duplicate_issues.length > 0) { - parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.map(d => - `  • <a href="https://github.com/microsoft/mssql-python/issues/${d.issue_number}">#${d.issue_number}</a> ${escVal(d.title)} [${d.state}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}` + parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.filter(d => /^\d+$/.test(String(d.issue_number))).map(d => + `  • <a href="https://github.com/microsoft/mssql-python/issues/${Number(d.issue_number)}">#${Number(d.issue_number)}</a> ${escVal(d.title)} [${escVal(d.state)}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}` ).join("<br>")}`); } if (parsed.recently_fixed && parsed.recently_fixed.length > 0) { - parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.map(f => - `  • <a href="https://github.com/microsoft/mssql-python/issues/${f.issue_number}">#${f.issue_number}</a> ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}` + parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.filter(f => /^\d+$/.test(String(f.issue_number))).map(f => + `  • <a href="https://github.com/microsoft/mssql-python/issues/${Number(f.issue_number)}">#${Number(f.issue_number)}</a> ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}` ).join("<br>")}`); } similarIssuesText = parts.join("<br><br>"); @@ -217,7 +217,7 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, s if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); if (parsed.has_workaround && parsed.workarounds && parsed.workarounds.length > 0) { parts.push(`<b>Workarounds:</b><br>${parsed.workarounds.map((w, i) => - `  ${i + 1}. <b>${escVal(w.description)}</b> [${w.confidence} confidence]<br>    Limitations: ${escVal(w.limitations)}${w.code_snippet ? `<br>    Code: <code>${escVal(w.code_snippet)}</code>` : ''}` + `  ${i + 1}. <b>${escVal(w.description)}</b> [${escVal(w.confidence)} confidence]<br>    Limitations: ${escVal(w.limitations)}${w.code_snippet ? `<br>    Code: <code>${escVal(w.code_snippet)}</code>` : ''}` ).join("<br>")}`); } if (parsed.can_downgrade && parsed.downgrade_version) { From f1066e96e62f1ad95bbda40269e8dcb4e858f107 Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai <sumitsar@microsoft.com> Date: Thu, 23 Apr 2026 17:35:19 +0530 Subject: [PATCH 3/4] Fix: Validate workaround confidence against allowlist (high|medium|low) - Replace @html/@escVal escaping of confidence with strict allowlist validation - Only 'high', 'medium', 'low' are accepted; anything else renders as 'unknown' - Prevents HTML injection via unexpected confidence values from LLM output - Applied in both issue-notify.yml (jq) and test-triage-local.js --- .github/workflows/issue-notify.yml | 2 +- test-triage-local.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-notify.yml b/.github/workflows/issue-notify.yml index 5167636d8..15e38df87 100644 --- a/.github/workflows/issue-notify.yml +++ b/.github/workflows/issue-notify.yml @@ -199,7 +199,7 @@ jobs: [ (if .summary then "<b>Summary:</b> " + (.summary | @html) else empty end), (if .has_workaround and .workarounds and (.workarounds | length) > 0 - then "<b>Workarounds:</b><br>" + ([.workarounds | to_entries[] | "  " + ((.key + 1) | tostring) + ". <b>" + (.value.description | @html) + "</b> [" + (.value.confidence | @html) + " confidence]<br>    Limitations: " + (.value.limitations | @html) + (if .value.code_snippet and (.value.code_snippet | length) > 0 then "<br>    Code: <code>" + (.value.code_snippet | @html) + "</code>" else "" end)] | join("<br>")) + then "<b>Workarounds:</b><br>" + ([.workarounds | to_entries[] | "  " + ((.key + 1) | tostring) + ". <b>" + (.value.description | @html) + "</b> [" + (if (.value.confidence | tostring) == "high" or (.value.confidence | tostring) == "medium" or (.value.confidence | tostring) == "low" then (.value.confidence | tostring) else "unknown" end) + " confidence]<br>    Limitations: " + (.value.limitations | @html) + (if .value.code_snippet and (.value.code_snippet | length) > 0 then "<br>    Code: <code>" + (.value.code_snippet | @html) + "</code>" else "" end)] | join("<br>")) else empty end), (if .can_downgrade == true and .downgrade_version then "⬇️ <b>Safe to downgrade to:</b> " + (.downgrade_version | @html) diff --git a/test-triage-local.js b/test-triage-local.js index 0ca55b200..1f0f71fce 100644 --- a/test-triage-local.js +++ b/test-triage-local.js @@ -216,8 +216,9 @@ async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, s const parts = []; if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); if (parsed.has_workaround && parsed.workarounds && parsed.workarounds.length > 0) { + const validConfidence = ['high', 'medium', 'low']; parts.push(`<b>Workarounds:</b><br>${parsed.workarounds.map((w, i) => - `  ${i + 1}. <b>${escVal(w.description)}</b> [${escVal(w.confidence)} confidence]<br>    Limitations: ${escVal(w.limitations)}${w.code_snippet ? `<br>    Code: <code>${escVal(w.code_snippet)}</code>` : ''}` + `  ${i + 1}. <b>${escVal(w.description)}</b> [${validConfidence.includes(w.confidence) ? w.confidence : 'unknown'} confidence]<br>    Limitations: ${escVal(w.limitations)}${w.code_snippet ? `<br>    Code: <code>${escVal(w.code_snippet)}</code>` : ''}` ).join("<br>")}`); } if (parsed.can_downgrade && parsed.downgrade_version) { From ff1b069fab7447e42579d1490d172cb35213b5ba Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai <sumitsar@microsoft.com> Date: Fri, 24 Apr 2026 13:53:34 +0530 Subject: [PATCH 4/4] Remove test-triage-local.js from repo and add to .gitignore --- .gitignore | 3 + test-triage-local.js | 695 ------------------------------------------- 2 files changed, 3 insertions(+), 695 deletions(-) delete mode 100644 test-triage-local.js diff --git a/.gitignore b/.gitignore index 5f7378b61..7123641f3 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ learnings/ # Triage test output files triage-out*.txt triage-err*.txt + +# Local triage test script +test-triage-local.js diff --git a/test-triage-local.js b/test-triage-local.js deleted file mode 100644 index 1f0f71fce..000000000 --- a/test-triage-local.js +++ /dev/null @@ -1,695 +0,0 @@ -/** - * Local test script for issue-triage + issue-notify workflows. - * Tests the exact same logic: fetch issue → GitHub Models classification → Teams notification. - * - * Prerequisites: Node.js 18+ (for native fetch) - * - * Usage: - * $env:GH_TOKEN = "ghp_your_pat_here" # needs models:read scope - * $env:TEAMS_WEBHOOK_URL = "https://your-webhook-url" - * node test-triage-local.js <issue_number> - */ - -const REPO_OWNER = "microsoft"; -const REPO_NAME = "mssql-python"; - -// --- Validate environment --- -const requiredEnv = ["GH_TOKEN", "TEAMS_WEBHOOK_URL"]; -for (const key of requiredEnv) { - if (!process.env[key]) { - console.error(`ERROR: Missing environment variable: ${key}`); - process.exit(1); - } -} - -const issueNumber = parseInt(process.argv[2]); -if (!issueNumber) { - console.error("Usage: node test-triage-local.js <issue_number>"); - process.exit(1); -} - -// --- Helper: GitHub Models --- -async function callGitHubModels(prompt) { - const token = process.env.GH_TOKEN; - const url = "https://models.github.ai/inference/chat/completions"; - - const response = await fetch(url, { - method: "POST", - headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: "openai/gpt-4.1", - messages: [ - { role: "system", content: "You are an expert assistant. Always respond in valid json format." }, - { role: "user", content: prompt }, - ], - temperature: 0.1, - response_format: { type: "json_object" }, - }), - }); - - if (!response.ok) { - const errText = await response.text(); - throw new Error(`GitHub Models error: ${response.status} - ${errText}`); - } - - const data = await response.json(); - return data.choices[0].message.content; -} - -// --- Helper: Fetch issue from GitHub --- -async function fetchIssue(issueNum) { - const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issueNum}`; - const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "triage-test" }; - - // Use GH_TOKEN if available for higher rate limits - if (process.env.GH_TOKEN) { - headers["Authorization"] = `token ${process.env.GH_TOKEN}`; - } - - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} - ${await response.text()}`); - } - return response.json(); -} - -// --- Helper: Fetch file content from GitHub --- -async function fetchFileContent(filePath) { - const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/${filePath}`; - const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "triage-test" }; - - if (process.env.GH_TOKEN) { - headers["Authorization"] = `token ${process.env.GH_TOKEN}`; - } - - const response = await fetch(url, { headers }); - if (!response.ok) { - throw new Error(`Could not fetch ${filePath}: ${response.status}`); - } - const data = await response.json(); - return Buffer.from(data.content, "base64").toString(); -} - -// --- Helper: Send Teams notification --- -async function sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, suggestedResponse, similarIssuesAnalysis, workaroundAnalysis, issue) { - const category = analysis.category; - const severity = analysis.severity; - - let emoji, categoryDisplay, action; - switch (category) { - case "FEATURE_REQUEST": - emoji = "💡"; categoryDisplay = "Feature Request"; - action = "Evaluate against roadmap. If approved, create ADO work item."; - break; - case "BUG": - emoji = "🐛"; categoryDisplay = "Bug"; - action = "Validate bug, reproduce if possible, assign to developer."; - break; - case "DISCUSSION": - emoji = "💬"; categoryDisplay = "Discussion"; - action = "Respond with guidance. Re-classify if needed."; - break; - case "BREAK_FIX": - emoji = "🚨"; categoryDisplay = "Break/Fix (Regression)"; - action = "URGENT: Assign to senior dev, create P0/P1 ADO item."; - break; - default: - emoji = "❓"; categoryDisplay = "Unknown"; - action = "Review and manually classify this issue."; - } - - const sevIndicator = severity === "critical" ? "🔴" - : severity === "high" ? "🟠" - : severity === "medium" ? "🟡" : "🟢"; - - const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - let codeAnalysisText = "N/A — classification did not require code analysis."; - let engineerGuidanceText = ""; - - if (codeAnalysis) { - try { - const parsed = JSON.parse(codeAnalysis); - - // Format code analysis as HTML with bold labels - const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - const parts = []; - if (parsed.is_bug) parts.push(`<b>Verdict:</b> ${escVal(parsed.is_bug)}`); - if (parsed.root_cause) parts.push(`<b>Root Cause:</b> ${escVal(parsed.root_cause)}`); - if (parsed.affected_components && parsed.affected_components.length > 0) { - parts.push(`<b>Affected Components:</b><br>${parsed.affected_components.map(c => `  • ${escVal(c)}`).join("<br>")}`); - } - if (parsed.evidence_and_context) parts.push(`<b>Evidence & Context:</b> ${escVal(parsed.evidence_and_context)}`); - if (parsed.recommended_fixes && parsed.recommended_fixes.length > 0) { - parts.push(`<b>Recommended Fixes:</b><br>${parsed.recommended_fixes.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("<br>")}`); - } - if (parsed.code_locations && parsed.code_locations.length > 0) { - parts.push(`<b>Code Locations:</b><br>${parsed.code_locations.map(l => `  • ${escVal(l)}`).join("<br>")}`); - } - if (parsed.risk_assessment) parts.push(`<b>Risk Assessment:</b> ${escVal(parsed.risk_assessment)}`); - codeAnalysisText = parts.join("<br><br>"); - } catch (e) { - codeAnalysisText = esc(codeAnalysis); - if (codeAnalysisText.length > 3000) { - codeAnalysisText = codeAnalysisText.slice(0, 3000) + "... (truncated)"; - } - } - } - - if (engineerGuidance) { - try { - const parsed = JSON.parse(engineerGuidance); - const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - const parts = []; - if (parsed.technical_assessment) parts.push(`<b>Technical Assessment:</b> ${escVal(parsed.technical_assessment)}`); - if (parsed.verdict) parts.push(`<b>Verdict:</b> ${escVal(parsed.verdict)}`); - if (parsed.effort_estimate) parts.push(`Effort Estimate: ${escVal(parsed.effort_estimate)}`); - if (parsed.affected_files && parsed.affected_files.length > 0) { - parts.push(`<b>Affected Files:</b><br>${parsed.affected_files.map(a => `  • ${escVal(a)}`).join("<br>")}`); - } - if (parsed.implementation_approach) parts.push(`<b>Implementation Approach:</b> ${escVal(parsed.implementation_approach)}`); - if (parsed.risks_and_tradeoffs) parts.push(`<b>Risks & Tradeoffs:</b> ${escVal(parsed.risks_and_tradeoffs)}`); - if (parsed.related_considerations && parsed.related_considerations.length > 0) { - parts.push(`<b>Related Considerations:</b><br>${parsed.related_considerations.map((s, i) => `  ${i + 1}. ${escVal(s)}`).join("<br>")}`); - } - engineerGuidanceText = parts.join("<br><br>"); - } catch (e) { - engineerGuidanceText = esc(engineerGuidance); - if (engineerGuidanceText.length > 3000) { - engineerGuidanceText = engineerGuidanceText.slice(0, 3000) + "... (truncated)"; - } - } - } - - // Format similar issues analysis - let similarIssuesText = ""; - if (similarIssuesAnalysis) { - try { - const parsed = JSON.parse(similarIssuesAnalysis); - const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - const parts = []; - if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); - if (parsed.duplicate_issues && parsed.duplicate_issues.length > 0) { - parts.push(`<b>Similar/Duplicate Issues:</b><br>${parsed.duplicate_issues.filter(d => /^\d+$/.test(String(d.issue_number))).map(d => - `  • <a href="https://github.com/microsoft/mssql-python/issues/${Number(d.issue_number)}">#${Number(d.issue_number)}</a> ${escVal(d.title)} [${escVal(d.state)}] — <i>${escVal(d.similarity)}:</i> ${escVal(d.explanation)}` - ).join("<br>")}`); - } - if (parsed.recently_fixed && parsed.recently_fixed.length > 0) { - parts.push(`<b>Recently Fixed:</b><br>${parsed.recently_fixed.filter(f => /^\d+$/.test(String(f.issue_number))).map(f => - `  • <a href="https://github.com/microsoft/mssql-python/issues/${Number(f.issue_number)}">#${Number(f.issue_number)}</a> ${escVal(f.title)} (closed ${escVal(f.closed_at)}) — ${escVal(f.relevance)}` - ).join("<br>")}`); - } - similarIssuesText = parts.join("<br><br>"); - } catch (e) { - similarIssuesText = esc(similarIssuesAnalysis); - } - } - - // Format workaround analysis - let workaroundText = ""; - if (workaroundAnalysis) { - try { - const parsed = JSON.parse(workaroundAnalysis); - const escVal = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); - const parts = []; - if (parsed.summary) parts.push(`<b>Summary:</b> ${escVal(parsed.summary)}`); - if (parsed.has_workaround && parsed.workarounds && parsed.workarounds.length > 0) { - const validConfidence = ['high', 'medium', 'low']; - parts.push(`<b>Workarounds:</b><br>${parsed.workarounds.map((w, i) => - `  ${i + 1}. <b>${escVal(w.description)}</b> [${validConfidence.includes(w.confidence) ? w.confidence : 'unknown'} confidence]<br>    Limitations: ${escVal(w.limitations)}${w.code_snippet ? `<br>    Code: <code>${escVal(w.code_snippet)}</code>` : ''}` - ).join("<br>")}`); - } - if (parsed.can_downgrade && parsed.downgrade_version) { - parts.push(`⬇️ <b>Safe to downgrade to:</b> ${escVal(parsed.downgrade_version)}`); - } - workaroundText = parts.join("<br><br>"); - } catch (e) { - workaroundText = esc(workaroundAnalysis); - } - } - - const htmlMessage = [ - `<h2>${emoji} mssql-python Issue Triage</h2>`, - `<p><b>${esc(categoryDisplay)}</b>  |  `, - `${sevIndicator} Severity: <b>${esc(severity)}</b>  |  `, - `Confidence: <b>${analysis.confidence}%</b></p>`, - `<hr>`, - `<p>`, - `📌 <b>Issue:</b> <a href="${issue.html_url}">#${issue.number} — ${esc(issue.title)}</a><br>`, - `👤 <b>Author:</b> @${esc(issue.user.login)}<br>`, - `🏷️ <b>Keywords:</b> ${esc(analysis.keywords.join(", "))}<br>`, - `📂 <b>Relevant Files:</b> ${esc(analysis.relevant_source_files.join(", "))}`, - `</p>`, - `<hr>`, - `<h3>📝 Analysis</h3>`, - `<p>${esc(analysis.summary_for_maintainers)}</p>`, - `<h3>🔍 Code Analysis</h3>`, - `<p>${codeAnalysisText}</p>`, - similarIssuesText ? `<h3>🔄 Similar Issues & Recent Fixes</h3>` : '', - similarIssuesText ? `<p>${similarIssuesText}</p>` : '', - workaroundText ? `<h3>🛠️ Workarounds</h3>` : '', - workaroundText ? `<p>${workaroundText}</p>` : '', - engineerGuidanceText ? `<h3>💡 Engineer Guidance</h3>` : '', - engineerGuidanceText ? `<p>${engineerGuidanceText}</p>` : '', - suggestedResponse ? `<hr>` : '', - suggestedResponse ? `<h3>✉️ Suggested Response to Customer</h3>` : '', - suggestedResponse ? `<p><i>Copy-paste or edit the response below and post it on the issue:</i></p>` : '', - suggestedResponse ? `<blockquote>${esc(suggestedResponse)}</blockquote>` : '', - `<hr>`, - `<p>⚡ <b>Action Required:</b> ${esc(action)}</p>`, - `<p><i>⚠️ AI-generated analysis — verified against source code but may contain inaccuracies. Review before acting.</i></p>`, - `<p><a href="${issue.html_url}">📋 View Issue</a>`, - `  |  `, - `<a href="https://github.com/${REPO_OWNER}/${REPO_NAME}">📂 View Repository</a></p>`, - ].join(""); - - const payload = { text: htmlMessage }; - - const response = await fetch(process.env.TEAMS_WEBHOOK_URL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Teams webhook error: ${response.status} - ${text}`); - } - - return response.status; -} - -// --- Main --- -async function main() { - console.log(`\n🔍 Fetching issue #${issueNumber}...`); - const issue = await fetchIssue(issueNumber); - console.log(` Title: ${issue.title}`); - console.log(` Author: ${issue.user.login}`); - - console.log(`\n🤖 Classifying with GitHub Models...`); - const classificationPrompt = ` -You are an expert triage system for the mssql-python repository — a Python driver for Microsoft SQL Server. -The driver uses ODBC under the hood with a C++/pybind11 native extension layer and Python wrappers. -Note: The pybind/ directory contains C++/pybind11 code (NOT Rust). Only reference Rust if the issue is specifically about BCP (Bulk Copy Protocol). - -Key source files in the repo: -- mssql_python/connection.py — Connection management, pooling integration -- mssql_python/cursor.py — Cursor operations, execute, fetch, bulkcopy -- mssql_python/auth.py — Authentication (SQL auth, Azure AD, etc.) -- mssql_python/exceptions.py — Error handling and exception classes -- mssql_python/pooling.py — Connection pooling -- mssql_python/helpers.py — Utility functions -- mssql_python/constants.py — Constants, SQL types, enums -- mssql_python/connection_string_parser.py — Connection string parsing -- mssql_python/parameter_helper.py — Query parameter handling -- mssql_python/logging.py — Logging infrastructure -- mssql_python/row.py — Row objects -- mssql_python/type.py — Type mappings -- mssql_python/ddbc_bindings.py — Python/pybind11 ODBC bindings (C++ native extension, NOT Rust) -- mssql_python/pybind/ — C++/pybind11 native extension layer (NOT Rust) - -Classify the following GitHub issue into EXACTLY ONE category: - -1. FEATURE_REQUEST — User wants new functionality or enhancements -2. BUG — Something is broken, incorrect behavior, or errors -3. DISCUSSION — User is asking a question or wants clarification -4. BREAK_FIX — A regression or critical bug: segfaults, crashes, data corruption, - or user says "this used to work" - -Respond in this exact JSON format: -{ - "category": "BUG|FEATURE_REQUEST|DISCUSSION|BREAK_FIX", - "confidence": <0-100>, - "justification": "<2-3 sentence explanation>", - "severity": "critical|high|medium|low", - "relevant_source_files": ["<top 3 most relevant source file paths>"], - "keywords": ["<key technical terms from the issue>"], - "summary_for_maintainers": "<detailed 3-5 sentence analysis for maintainer notification>" -} - -Issue Title: ${issue.title} -Issue Body: -${(issue.body || "").slice(0, 4000)} -`; - - const classifyResult = await callGitHubModels(classificationPrompt); - const analysis = JSON.parse(classifyResult); - - console.log(`\n📊 Classification Results:`); - console.log(` Category: ${analysis.category}`); - console.log(` Confidence: ${analysis.confidence}%`); - console.log(` Severity: ${analysis.severity}`); - console.log(` Keywords: ${analysis.keywords.join(", ")}`); - console.log(` Files: ${analysis.relevant_source_files.join(", ")}`); - console.log(` Summary: ${analysis.summary_for_maintainers}`); - - // --- Fetch relevant source files (for ALL categories) --- - console.log(`\n📂 Fetching relevant source files for code-grounded analysis...`); - const fileContents = []; - for (const filePath of analysis.relevant_source_files.slice(0, 3)) { - try { - const content = await fetchFileContent(filePath); - fileContents.push(`### File: ${filePath}\n\`\`\`python\n${content.slice(0, 3000)}\n\`\`\``); - console.log(` ✅ Fetched ${filePath}`); - } catch (e) { - console.log(` ⚠️ Could not fetch ${filePath}: ${e.message}`); - } - } - - const codeContext = fileContents.length > 0 - ? `\n\nRelevant source files from the repository:\n${fileContents.join("\n\n")}` - : ''; - - // --- For BUG/BREAK_FIX, analyze codebase --- - let codeAnalysis = ""; - - if (["BUG", "BREAK_FIX"].includes(analysis.category) && fileContents.length > 0) { - console.log(`\n🔬 Bug/Break-fix detected — analyzing codebase...`); - - const codePrompt = ` -You are a senior Python developer analyzing a potential -${analysis.category === "BREAK_FIX" ? "regression/break-fix" : "bug"} -in the mssql-python driver (Python + ODBC + C++/pybind11 native layer). -IMPORTANT: ddbc_bindings.py and the pybind/ directory are C++/pybind11 code, NOT Rust. Only mention Rust if the issue is specifically about BCP (Bulk Copy Protocol). -IMPORTANT: Base your analysis ONLY on the actual source code provided below. Do not speculate about code you haven't seen. - -Bug Report: -Title: ${issue.title} -Body: ${(issue.body || "").slice(0, 2000)} -${codeContext} - -Provide analysis in JSON: -{ - "is_bug": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", - "root_cause": "<detailed root cause analysis based on actual code above>", - "affected_components": ["<affected modules/functions from the code above>"], - "evidence_and_context": "<specific evidence from the codebase — cite exact functions, variables, line logic, or patterns that support your analysis>", - "recommended_fixes": ["<fix 1 — describe the approach referencing specific code>", "<fix 2>", "<fix 3>"], - "code_locations": ["<file:function or file:class where changes should be made>"], - "risk_assessment": "<risk to users>" -} -`; - - try { - codeAnalysis = await callGitHubModels(codePrompt); - const parsed = JSON.parse(codeAnalysis); - console.log(`\n🔍 Code Analysis:`); - console.log(` Is Bug: ${parsed.is_bug}`); - console.log(` Root Cause: ${parsed.root_cause}`); - if (parsed.evidence_and_context) { - console.log(` Evidence: ${parsed.evidence_and_context}`); - } - if (parsed.recommended_fixes && parsed.recommended_fixes.length > 0) { - console.log(`\n\ud83d\udee0\ufe0f Recommended Fixes:`); - for (const fix of parsed.recommended_fixes) { - console.log(` \u2022 ${fix}`); - } - } - if (parsed.code_locations && parsed.code_locations.length > 0) { - console.log(`\n\ud83d\udccd Code Locations:`); - for (const loc of parsed.code_locations) { - console.log(` \u2022 ${loc}`); - } - } - } catch (e) { - console.log(` ⚠️ Code analysis failed: ${e.message}`); - } - } - - // --- For FEATURE_REQUEST/DISCUSSION, provide code-grounded engineer guidance --- - let engineerGuidance = ""; - - if (["FEATURE_REQUEST", "DISCUSSION"].includes(analysis.category)) { - console.log(`\n💡 Non-bug issue — generating code-grounded engineer guidance...`); - - const guidancePrompt = ` -You are a senior engineer on the mssql-python team — a Python driver for Microsoft SQL Server -(ODBC + C++/pybind11 native extension + Python wrappers). -IMPORTANT: Base your analysis ONLY on the actual source code provided below. Do not speculate about code you haven't seen. If the code doesn't contain enough information, say so explicitly. - -A user filed a GitHub issue classified as: ${analysis.category} - -Issue Title: ${issue.title} -Issue Body: -${(issue.body || "").slice(0, 3000)} -${codeContext} - -Based on the ACTUAL SOURCE CODE above, provide a detailed analysis to help the engineering team respond efficiently. -Respond in JSON: -{ - "technical_assessment": "<detailed technical assessment grounded in the actual code above>", - "verdict": "Confirmed Bug|Likely Bug|Require More Analysis|Not a Bug", - "issue_identified": true/false, - "affected_files": ["<specific source files, modules, functions, or classes from the code above>"], - "current_behavior": "<describe what the current code actually does based on your reading>", - "implementation_approach": "<concrete implementation steps referencing specific functions/lines from the code — ONLY if issue_identified is true, otherwise empty string>", - "effort_estimate": "small|medium|large|epic", - "risks_and_tradeoffs": "<potential risks, backward compatibility concerns, or tradeoffs — ONLY if issue_identified is true, otherwise empty string>", - "related_considerations": ["<other things the team should think about — ONLY if issue_identified is true, otherwise empty array>"] -} - -IMPORTANT: If your technical_assessment does not identify any actual issue or gap in the code, set issue_identified to false and leave implementation_approach, risks_and_tradeoffs, and related_considerations empty. Only populate those fields when a real problem or improvement opportunity is confirmed in the code. -`; - - try { - engineerGuidance = await callGitHubModels(guidancePrompt); - const parsed = JSON.parse(engineerGuidance); - console.log(`\n💡 Engineer Guidance:`); - console.log(` Verdict: ${parsed.verdict}`); - console.log(` Effort: ${parsed.effort_estimate}`); - console.log(` Current Code: ${parsed.current_behavior}`); - console.log(` Assessment: ${parsed.technical_assessment}`); - console.log(` Approach: ${parsed.implementation_approach}`); - console.log(` Risks: ${parsed.risks_and_tradeoffs}`); - } catch (e) { - console.log(` ⚠️ Engineer guidance failed: ${e.message}`); - } - } - - // --- Search for similar/duplicate issues and recent fixes --- - console.log(`\n🔄 Searching for similar issues and recent fixes...`); - let similarIssuesAnalysis = ''; - - try { - const headers = { "Accept": "application/vnd.github.v3+json", "User-Agent": "triage-test" }; - if (process.env.GH_TOKEN) headers["Authorization"] = `token ${process.env.GH_TOKEN}`; - - const openRes = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=open&per_page=30&sort=created&direction=desc`, { headers }); - const openIssues = await openRes.json(); - - const closedRes = await fetch(`https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=closed&per_page=30&sort=updated&direction=desc`, { headers }); - const closedIssues = await closedRes.json(); - - const allIssues = [...openIssues, ...closedIssues] - .filter(i => i.number !== issueNumber && !i.pull_request) - .map(i => ({ - number: i.number, - title: i.title, - state: i.state, - labels: i.labels.map(l => l.name).join(', '), - created_at: i.created_at, - closed_at: i.closed_at, - body_preview: (i.body || '').slice(0, 300) - })); - - console.log(` Found ${allIssues.length} issues to compare against`); - - const similarPrompt = ` -You are an expert triage system for the mssql-python repository. -A new issue has been filed. Your job is to check if any similar or duplicate issues -already exist (open or recently closed/fixed). - -NEW ISSUE: -Title: ${issue.title} -Body: ${(issue.body || '').slice(0, 2000)} -Keywords: ${analysis.keywords.join(', ')} - -EXISTING ISSUES (recent open + recently closed): -${JSON.stringify(allIssues, null, 2).slice(0, 6000)} - -Analyze and respond in JSON: -{ - "has_duplicates": true/false, - "has_similar": true/false, - "duplicate_issues": [ - { - "issue_number": <number>, - "title": "<title>", - "state": "open|closed", - "similarity": "duplicate|highly_similar|related", - "explanation": "<why this is a duplicate/similar>" - } - ], - "recently_fixed": [ - { - "issue_number": <number>, - "title": "<title>", - "closed_at": "<date>", - "relevance": "<how this fix relates to the new issue>" - } - ], - "summary": "<1-3 sentence summary: are there duplicates? was this recently fixed? should the new issue be closed as duplicate?>" -} - -IMPORTANT: -- Only flag as duplicate if the core problem is truly the same, not just topically related -- For recently_fixed, only include issues that were closed AND directly address the same problem -- If no duplicates or similar issues exist, set has_duplicates and has_similar to false with empty arrays -`; - - similarIssuesAnalysis = await callGitHubModels(similarPrompt); - const parsed = JSON.parse(similarIssuesAnalysis); - console.log(` Duplicates: ${parsed.has_duplicates}, Similar: ${parsed.has_similar}`); - console.log(` Summary: ${parsed.summary}`); - } catch (e) { - console.log(` ⚠️ Similar issues search failed: ${e.message}`); - } - - // --- Generate workaround suggestions (BUG/BREAK_FIX only) --- - let workaroundAnalysis = ''; - - if (["BUG", "BREAK_FIX"].includes(analysis.category)) { - console.log(`\n🛠️ Generating workaround suggestions...`); - - const workaroundCtx = codeAnalysis ? `Code Analysis:\n${codeAnalysis}` : ''; - - const workaroundPrompt = ` -You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. -A user has reported a ${analysis.category === "BREAK_FIX" ? "regression/break-fix" : "bug"}. -Your job is to suggest practical workarounds the user can apply RIGHT NOW while the team works on a proper fix. - -Issue Title: ${issue.title} -Issue Body: -${(issue.body || "").slice(0, 2000)} - -${workaroundCtx} -${codeContext} - -Provide workaround suggestions in JSON: -{ - "has_workaround": true/false, - "workarounds": [ - { - "description": "<clear description of the workaround>", - "code_snippet": "<Python code snippet showing the workaround, if applicable>", - "limitations": "<any limitations or caveats of this workaround>", - "confidence": "high|medium|low" - } - ], - "can_downgrade": true/false, - "downgrade_version": "<safe version to downgrade to, if applicable>", - "summary": "<1-2 sentence summary of available workarounds>" -} - -IMPORTANT: -- Only suggest workarounds you are confident about based on the code and issue description -- Workarounds should be practical and safe for production use -- If no reliable workaround exists, set has_workaround to false -- For BREAK_FIX issues, always consider if downgrading to a previous version is viable -- Code snippets should be complete and copy-pasteable -`; - - try { - workaroundAnalysis = await callGitHubModels(workaroundPrompt); - const parsed = JSON.parse(workaroundAnalysis); - console.log(` Has workaround: ${parsed.has_workaround}`); - console.log(` Summary: ${parsed.summary}`); - if (parsed.workarounds && parsed.workarounds.length > 0) { - for (const w of parsed.workarounds) { - console.log(` • ${w.description} [${w.confidence}]`); - } - } - } catch (e) { - console.log(` ⚠️ Workaround generation failed: ${e.message}`); - } - } - - // --- Generate suggested customer response (all categories) --- - console.log(`\n✉️ Generating suggested customer response...`); - let suggestedResponse = ''; - - const analysisContext = codeAnalysis - ? `Code Analysis:\n${codeAnalysis}` - : engineerGuidance - ? `Engineer Guidance:\n${engineerGuidance}` - : ''; - - const workaroundResponseCtx = workaroundAnalysis - ? `Workaround Analysis:\n${workaroundAnalysis}` - : ''; - - const similarResponseCtx = similarIssuesAnalysis - ? `Similar Issues Analysis:\n${similarIssuesAnalysis}` - : ''; - - const responsePrompt = ` -You are a senior support engineer on the mssql-python team — Microsoft's Python driver for SQL Server. -A customer filed a GitHub issue and you need to craft a helpful, professional, and empathetic response -that an engineer can copy-paste (or lightly edit) and post on the issue. - -Issue Category: ${analysis.category} -Severity: ${analysis.severity} -Issue Title: ${issue.title} -Issue Author: @${issue.user.login} -Issue Body: -${(issue.body || '').slice(0, 3000)} - -${analysisContext} - -${workaroundResponseCtx} - -${similarResponseCtx} - -Write a suggested response following these guidelines: -- Address the author by their GitHub username (@username) -- Thank them for filing the issue -- Acknowledge the specific problem or request they described -- For BUG/BREAK_FIX: Let them know the team is investigating; ask for OS, Python version, - mssql-python version, SQL Server version, and a minimal repro script if not already provided; - if workarounds are available, mention them briefly -- For FEATURE_REQUEST: Acknowledge the value of the request; mention the team will evaluate it; - ask for use-case details or code examples showing desired behavior if not provided -- For DISCUSSION: Provide helpful guidance or clarification based on the analysis; - point to relevant docs or code if applicable -- If similar or duplicate issues were found, mention them (e.g., "This looks related to #123") -- Always ask for a minimal reproduction script/code snippet if the user hasn't provided one -- Keep the tone warm, professional, and collaborative -- Use Markdown formatting suitable for GitHub comments -- Do NOT promise timelines or specific fixes -- Do NOT reveal internal triage details or AI involvement -- Keep it concise (under 200 words) - -Respond in JSON: -{ - "suggested_response": "<the full Markdown response ready to post on GitHub>" -} -`; - - try { - const responseResult = await callGitHubModels(responsePrompt); - const parsed = JSON.parse(responseResult); - suggestedResponse = parsed.suggested_response || ''; - console.log(` ✅ Suggested response generated`); - console.log(`\n✉️ Suggested Response:\n${suggestedResponse}`); - } catch (e) { - console.log(` ⚠️ Suggested response generation failed: ${e.message}`); - } - - // --- Send Teams notification --- - console.log(`\n📤 Sending Teams notification...`); - try { - const status = await sendTeamsNotification(analysis, codeAnalysis, engineerGuidance, suggestedResponse, similarIssuesAnalysis, workaroundAnalysis, issue); - console.log(` ✅ Teams notification sent (HTTP ${status})`); - } catch (e) { - console.error(` ❌ Teams notification failed: ${e.message}`); - } - - console.log(`\n✅ Triage complete for issue #${issueNumber}`); -} - -main().catch((e) => { - console.error(`\n❌ Fatal error: ${e.message}`); - process.exit(1); -});