@@ -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