Skip to content

Commit 03454f0

Browse files
Copilotreakaleek
andcommitted
Add resilience improvements: retry logic, content sanitization, and prompt limits
Co-authored-by: reakaleek <[email protected]>
1 parent d23b831 commit 03454f0

File tree

1 file changed

+95
-54
lines changed

1 file changed

+95
-54
lines changed

.github/workflows/detect-duplicate-issues.yml

Lines changed: 95 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -68,45 +68,84 @@ jobs:
6868
console.log('Analyzing ' + openIssues.length + ' existing issues for potential duplicates');
6969
7070
try {
71-
// Step 1: Send all issue titles and numbers to get top 5 candidates
72-
let titlePrompt = 'Analyze this NEW ISSUE against all EXISTING ISSUES and identify the top 5 most similar ones:\n\n';
71+
// Helper function to safely escape content for prompts
72+
function sanitizeContent(content) {
73+
if (!content) return 'No description provided';
74+
return content.replace(/[`'"\\]/g, ' ').slice(0, 500); // Limit length and escape problematic chars
75+
}
76+
77+
// Helper function to retry AI calls with exponential backoff
78+
async function retryApiCall(apiCallFn, maxRetries = 2) {
79+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
80+
try {
81+
const response = await apiCallFn();
82+
if (response.ok) return response;
83+
84+
if (attempt < maxRetries) {
85+
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s delays
86+
console.log('API call failed, retrying in ' + delay + 'ms (attempt ' + (attempt + 1) + '/' + (maxRetries + 1) + ')');
87+
await new Promise(resolve => setTimeout(resolve, delay));
88+
} else {
89+
return response; // Return the failed response on final attempt
90+
}
91+
} catch (error) {
92+
if (attempt === maxRetries) throw error;
93+
const delay = Math.pow(2, attempt) * 1000;
94+
console.log('API call error, retrying in ' + delay + 'ms: ' + error.message);
95+
await new Promise(resolve => setTimeout(resolve, delay));
96+
}
97+
}
98+
}
99+
100+
// Limit the number of issues to analyze to prevent token overflow
101+
const maxIssuesForAnalysis = Math.min(openIssues.length, 50); // Limit to 50 issues max
102+
const issuesToAnalyze = openIssues.slice(0, maxIssuesForAnalysis);
103+
104+
if (issuesToAnalyze.length < openIssues.length) {
105+
console.log('Limiting analysis to ' + maxIssuesForAnalysis + ' most recent issues (out of ' + openIssues.length + ' total)');
106+
}
107+
108+
// Step 1: Send issue titles and numbers to get top 5 candidates
109+
let titlePrompt = 'Analyze this NEW ISSUE against EXISTING ISSUES and identify the top 5 most similar ones:\n\n';
73110
titlePrompt += 'NEW ISSUE:\n';
74-
titlePrompt += 'Title: ' + newIssue.title + '\n';
75-
titlePrompt += 'Body: ' + (newIssue.body || 'No description provided') + '\n\n';
111+
titlePrompt += 'Title: ' + sanitizeContent(newIssue.title) + '\n';
112+
titlePrompt += 'Body: ' + sanitizeContent(newIssue.body) + '\n\n';
76113
titlePrompt += 'EXISTING ISSUES:\n';
77114
78-
openIssues.forEach((issue, index) => {
79-
titlePrompt += (index + 1) + '. Issue #' + issue.number + ' - ' + issue.title + '\n';
115+
issuesToAnalyze.forEach((issue, index) => {
116+
titlePrompt += (index + 1) + '. Issue #' + issue.number + ' - ' + sanitizeContent(issue.title) + '\n';
80117
});
81118
82119
titlePrompt += '\nRespond with a JSON object containing the top 5 most similar issues. Format: {"similar_issues": [{"rank": 1, "issue_number": 123, "similarity": "high|medium"}, ...]}';
83120
84-
const titleResponse = await fetch('https://models.inference.ai.azure.com/chat/completions', {
85-
method: 'POST',
86-
headers: {
87-
'Authorization': 'Bearer ' + github.token,
88-
'Content-Type': 'application/json',
89-
},
90-
body: JSON.stringify({
91-
messages: [
92-
{
93-
role: 'system',
94-
content: 'You are an expert at analyzing GitHub issues to detect duplicates. Compare issue titles and descriptions to identify the most similar ones. Respond only with valid JSON containing the top 5 most similar issues ranked by relevance. Use "high" for likely duplicates and "medium" for related issues.'
95-
},
96-
{
97-
role: 'user',
98-
content: titlePrompt
99-
}
100-
],
101-
model: 'gpt-4o-mini',
102-
temperature: 0.1,
103-
max_tokens: 200
121+
const titleResponse = await retryApiCall(() =>
122+
fetch('https://models.inference.ai.azure.com/chat/completions', {
123+
method: 'POST',
124+
headers: {
125+
'Authorization': 'Bearer ' + github.token,
126+
'Content-Type': 'application/json',
127+
},
128+
body: JSON.stringify({
129+
messages: [
130+
{
131+
role: 'system',
132+
content: 'You are an expert at analyzing GitHub issues to detect duplicates. Compare issue titles and descriptions to identify the most similar ones. Respond only with valid JSON containing the top 5 most similar issues ranked by relevance. Use "high" for likely duplicates and "medium" for related issues.'
133+
},
134+
{
135+
role: 'user',
136+
content: titlePrompt
137+
}
138+
],
139+
model: 'gpt-4o-mini',
140+
temperature: 0.1,
141+
max_tokens: 200
142+
})
104143
})
105-
});
144+
);
106145
107146
if (!titleResponse.ok) {
108147
const errorText = await titleResponse.text();
109-
console.log('First AI call failed: ' + titleResponse.status + ' - ' + errorText);
148+
console.log('First AI call failed after retries: ' + titleResponse.status + ' - ' + errorText);
110149
return;
111150
}
112151
@@ -161,40 +200,42 @@ jobs:
161200
// Step 3: Detailed analysis with full issue bodies
162201
let detailPrompt = 'Perform detailed comparison of this NEW ISSUE against the TOP CANDIDATE ISSUES:\n\n';
163202
detailPrompt += 'NEW ISSUE:\n';
164-
detailPrompt += 'Title: ' + newIssue.title + '\n';
165-
detailPrompt += 'Body: ' + (newIssue.body || 'No description provided') + '\n\n';
203+
detailPrompt += 'Title: ' + sanitizeContent(newIssue.title) + '\n';
204+
detailPrompt += 'Body: ' + sanitizeContent(newIssue.body) + '\n\n';
166205
detailPrompt += 'CANDIDATE ISSUES FOR DETAILED ANALYSIS:\n';
167206
168207
candidateIssues.forEach((candidate, index) => {
169208
detailPrompt += (index + 1) + '. Issue #' + candidate.issue.number + '\n';
170-
detailPrompt += ' Title: ' + candidate.issue.title + '\n';
171-
detailPrompt += ' Body: ' + (candidate.issue.body || 'No description provided') + '\n\n';
209+
detailPrompt += ' Title: ' + sanitizeContent(candidate.issue.title) + '\n';
210+
detailPrompt += ' Body: ' + sanitizeContent(candidate.issue.body) + '\n\n';
172211
});
173212
174213
detailPrompt += 'Respond with JSON format: {"duplicates": [{"issue_number": 123, "classification": "DUPLICATE|SIMILAR|DIFFERENT", "reason": "brief explanation"}]}';
175214
176-
const detailResponse = await fetch('https://models.inference.ai.azure.com/chat/completions', {
177-
method: 'POST',
178-
headers: {
179-
'Authorization': 'Bearer ' + github.token,
180-
'Content-Type': 'application/json',
181-
},
182-
body: JSON.stringify({
183-
messages: [
184-
{
185-
role: 'system',
186-
content: 'You are an expert at analyzing GitHub issues for duplicates. Compare the full content and determine: DUPLICATE (same core problem), SIMILAR (related but different aspects), or DIFFERENT (unrelated). Respond only with valid JSON.'
187-
},
188-
{
189-
role: 'user',
190-
content: detailPrompt
191-
}
192-
],
193-
model: 'gpt-4o-mini',
194-
temperature: 0.1,
195-
max_tokens: 300
215+
const detailResponse = await retryApiCall(() =>
216+
fetch('https://models.inference.ai.azure.com/chat/completions', {
217+
method: 'POST',
218+
headers: {
219+
'Authorization': 'Bearer ' + github.token,
220+
'Content-Type': 'application/json',
221+
},
222+
body: JSON.stringify({
223+
messages: [
224+
{
225+
role: 'system',
226+
content: 'You are an expert at analyzing GitHub issues for duplicates. Compare the full content and determine: DUPLICATE (same core problem), SIMILAR (related but different aspects), or DIFFERENT (unrelated). Respond only with valid JSON.'
227+
},
228+
{
229+
role: 'user',
230+
content: detailPrompt
231+
}
232+
],
233+
model: 'gpt-4o-mini',
234+
temperature: 0.1,
235+
max_tokens: 300
236+
})
196237
})
197-
});
238+
);
198239
199240
if (detailResponse.ok) {
200241
const detailResult = await detailResponse.json();
@@ -238,7 +279,7 @@ jobs:
238279
}
239280
} else {
240281
const errorText = await detailResponse.text();
241-
console.log('Detailed analysis failed: ' + detailResponse.status + ' - ' + errorText);
282+
console.log('Detailed analysis failed after retries: ' + detailResponse.status + ' - ' + errorText);
242283
}
243284
244285
} catch (error) {

0 commit comments

Comments
 (0)