|
16 | 16 | required: true |
17 | 17 | default: "0" |
18 | 18 | type: string |
| 19 | + ai_game_fallback: |
| 20 | + description: Use AI only when deterministic game mapping finds no game |
| 21 | + required: true |
| 22 | + default: "false" |
| 23 | + type: choice |
| 24 | + options: |
| 25 | + - "false" |
| 26 | + - "true" |
19 | 27 | issues: |
20 | 28 | types: |
21 | 29 | - opened |
@@ -893,18 +901,24 @@ jobs: |
893 | 901 | env: |
894 | 902 | ISSUE_STATE: ${{ inputs.issue_state }} |
895 | 903 | ISSUE_LIMIT: ${{ inputs.limit }} |
| 904 | + AI_GAME_FALLBACK: ${{ inputs.ai_game_fallback }} |
896 | 905 | steps: |
897 | 906 | - name: Trigger relabel backfill |
898 | 907 | uses: actions/github-script@v9 |
| 908 | + env: |
| 909 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
899 | 910 | with: |
900 | 911 | script: | |
901 | 912 | const owner = context.repo.owner; |
902 | 913 | const repo = context.repo.repo; |
903 | 914 | const state = process.env.ISSUE_STATE || 'all'; |
904 | 915 | const rawLimit = Number.parseInt(process.env.ISSUE_LIMIT || '0', 10); |
905 | 916 | const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 0; |
| 917 | + const useAiGameFallback = String(process.env.AI_GAME_FALLBACK || 'false').toLowerCase() === 'true'; |
906 | 918 | const processedIssues = []; |
907 | 919 | const failedIssues = []; |
| 920 | + let aiGameAttempts = 0; |
| 921 | + let aiGameMatches = 0; |
908 | 922 |
|
909 | 923 | // === Helpers (mirrored from issue-ai-maintenance) === |
910 | 924 |
|
@@ -943,6 +957,31 @@ jobs: |
943 | 957 | return { labels, scripts }; |
944 | 958 | } |
945 | 959 |
|
| 960 | + function parseAiGameResponse(raw) { |
| 961 | + const input = (raw || '').trim(); |
| 962 | + if (!input) return {}; |
| 963 | +
|
| 964 | + const candidates = [input]; |
| 965 | + const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i); |
| 966 | + if (fenced?.[1]) candidates.push(fenced[1].trim()); |
| 967 | +
|
| 968 | + const firstBrace = input.indexOf('{'); |
| 969 | + const lastBrace = input.lastIndexOf('}'); |
| 970 | + if (firstBrace !== -1 && lastBrace > firstBrace) { |
| 971 | + candidates.push(input.slice(firstBrace, lastBrace + 1)); |
| 972 | + } |
| 973 | +
|
| 974 | + for (const candidate of candidates) { |
| 975 | + try { |
| 976 | + return JSON.parse(candidate); |
| 977 | + } catch (_err) { |
| 978 | + // Continue trying fallbacks. |
| 979 | + } |
| 980 | + } |
| 981 | +
|
| 982 | + return {}; |
| 983 | + } |
| 984 | +
|
946 | 985 | function parseServerlistCsv(csvText) { |
947 | 986 | const rows = []; |
948 | 987 | const lines = (csvText || '').split(/\r?\n/); |
@@ -1254,6 +1293,65 @@ jobs: |
1254 | 1293 | for (const scriptName of fromText.scripts) desiredServerScripts.add(scriptName); |
1255 | 1294 | } |
1256 | 1295 |
|
| 1296 | + // Optional AI fallback for legacy issues where deterministic matching finds nothing. |
| 1297 | + if (useAiGameFallback && desiredGames.size === 0) { |
| 1298 | + aiGameAttempts += 1; |
| 1299 | + try { |
| 1300 | + const res = await fetch( |
| 1301 | + `https://models.github.ai/orgs/${owner}/inference/chat/completions`, |
| 1302 | + { |
| 1303 | + method: 'POST', |
| 1304 | + headers: { |
| 1305 | + Accept: 'application/vnd.github+json', |
| 1306 | + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, |
| 1307 | + 'X-GitHub-Api-Version': '2026-03-10', |
| 1308 | + 'Content-Type': 'application/json', |
| 1309 | + }, |
| 1310 | + body: JSON.stringify({ |
| 1311 | + model: 'openai/gpt-4.1-mini', |
| 1312 | + temperature: 0.1, |
| 1313 | + max_tokens: 120, |
| 1314 | + messages: [ |
| 1315 | + { |
| 1316 | + role: 'system', |
| 1317 | + content: |
| 1318 | + 'Return JSON only. Identify the game referenced in this LinuxGSM issue with high precision.', |
| 1319 | + }, |
| 1320 | + { |
| 1321 | + role: 'user', |
| 1322 | + content: |
| 1323 | + `Title: ${title}\n\nBody:\n${body.slice(0, 2500)}\n\n` + |
| 1324 | + 'Return JSON: {"detected_game":"string or null","game_confidence":"high|medium|low|null"}', |
| 1325 | + }, |
| 1326 | + ], |
| 1327 | + }), |
| 1328 | + } |
| 1329 | + ); |
| 1330 | +
|
| 1331 | + if (res.ok) { |
| 1332 | + const data = await res.json(); |
| 1333 | + const raw = data.choices?.[0]?.message?.content || '{}'; |
| 1334 | + const parsed = parseAiGameResponse(raw); |
| 1335 | + const detectedGame = normalizeName(parsed?.detected_game || ''); |
| 1336 | + const confidence = (parsed?.game_confidence || '').toLowerCase(); |
| 1337 | + if (detectedGame && confidence === 'high') { |
| 1338 | + const mappedLabel = |
| 1339 | + gameAliasToLabel.get(detectedGame) || gameLabelByNormalized.get(detectedGame); |
| 1340 | + if (mappedLabel) { |
| 1341 | + desiredGames.add(mappedLabel); |
| 1342 | + const mappedScript = gameAliasToScript.get(detectedGame); |
| 1343 | + if (mappedScript) desiredServerScripts.add(mappedScript); |
| 1344 | + aiGameMatches += 1; |
| 1345 | + } |
| 1346 | + } |
| 1347 | + } else { |
| 1348 | + console.log(`#${rawIssue.number}: AI fallback skipped (HTTP ${res.status})`); |
| 1349 | + } |
| 1350 | + } catch (err) { |
| 1351 | + console.log(`#${rawIssue.number}: AI fallback error: ${err.message}`); |
| 1352 | + } |
| 1353 | + } |
| 1354 | +
|
1257 | 1355 | for (const gameLabel of desiredGames) { |
1258 | 1356 | const mappedScript = gameAliasToScript.get(normalizeName(gameLabel.slice(6))); |
1259 | 1357 | if (mappedScript) desiredServerScripts.add(mappedScript); |
@@ -1352,13 +1450,19 @@ jobs: |
1352 | 1450 | [ |
1353 | 1451 | { data: 'Requested state', header: true }, |
1354 | 1452 | { data: 'Limit', header: true }, |
| 1453 | + { data: 'AI fallback', header: true }, |
| 1454 | + { data: 'AI attempts', header: true }, |
| 1455 | + { data: 'AI matches', header: true }, |
1355 | 1456 | { data: 'Target issues', header: true }, |
1356 | 1457 | { data: 'Processed', header: true }, |
1357 | 1458 | { data: 'Failures', header: true }, |
1358 | 1459 | ], |
1359 | 1460 | [ |
1360 | 1461 | state, |
1361 | 1462 | limit === 0 ? 'all' : String(limit), |
| 1463 | + useAiGameFallback ? 'enabled' : 'disabled', |
| 1464 | + String(aiGameAttempts), |
| 1465 | + String(aiGameMatches), |
1362 | 1466 | String(selectedTargets.length), |
1363 | 1467 | String(processed), |
1364 | 1468 | String(failedIssues.length), |
|
0 commit comments