Skip to content

Commit 54007fd

Browse files
committed
feat: add optional AI fallback for backfill game detection
Add workflow_dispatch input ai_game_fallback (default false). In backfill mode, only call AI when deterministic game mapping finds no match; accept only high-confidence results and map through known game aliases/labels. Include AI usage stats in the run summary table.
1 parent 6c1e80d commit 54007fd

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

.github/workflows/labeler.yml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ on:
1616
required: true
1717
default: "0"
1818
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"
1927
issues:
2028
types:
2129
- opened
@@ -893,18 +901,24 @@ jobs:
893901
env:
894902
ISSUE_STATE: ${{ inputs.issue_state }}
895903
ISSUE_LIMIT: ${{ inputs.limit }}
904+
AI_GAME_FALLBACK: ${{ inputs.ai_game_fallback }}
896905
steps:
897906
- name: Trigger relabel backfill
898907
uses: actions/github-script@v9
908+
env:
909+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
899910
with:
900911
script: |
901912
const owner = context.repo.owner;
902913
const repo = context.repo.repo;
903914
const state = process.env.ISSUE_STATE || 'all';
904915
const rawLimit = Number.parseInt(process.env.ISSUE_LIMIT || '0', 10);
905916
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 0;
917+
const useAiGameFallback = String(process.env.AI_GAME_FALLBACK || 'false').toLowerCase() === 'true';
906918
const processedIssues = [];
907919
const failedIssues = [];
920+
let aiGameAttempts = 0;
921+
let aiGameMatches = 0;
908922
909923
// === Helpers (mirrored from issue-ai-maintenance) ===
910924
@@ -943,6 +957,31 @@ jobs:
943957
return { labels, scripts };
944958
}
945959
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+
946985
function parseServerlistCsv(csvText) {
947986
const rows = [];
948987
const lines = (csvText || '').split(/\r?\n/);
@@ -1254,6 +1293,65 @@ jobs:
12541293
for (const scriptName of fromText.scripts) desiredServerScripts.add(scriptName);
12551294
}
12561295
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+
12571355
for (const gameLabel of desiredGames) {
12581356
const mappedScript = gameAliasToScript.get(normalizeName(gameLabel.slice(6)));
12591357
if (mappedScript) desiredServerScripts.add(mappedScript);
@@ -1352,13 +1450,19 @@ jobs:
13521450
[
13531451
{ data: 'Requested state', header: true },
13541452
{ data: 'Limit', header: true },
1453+
{ data: 'AI fallback', header: true },
1454+
{ data: 'AI attempts', header: true },
1455+
{ data: 'AI matches', header: true },
13551456
{ data: 'Target issues', header: true },
13561457
{ data: 'Processed', header: true },
13571458
{ data: 'Failures', header: true },
13581459
],
13591460
[
13601461
state,
13611462
limit === 0 ? 'all' : String(limit),
1463+
useAiGameFallback ? 'enabled' : 'disabled',
1464+
String(aiGameAttempts),
1465+
String(aiGameMatches),
13621466
String(selectedTargets.length),
13631467
String(processed),
13641468
String(failedIssues.length),

0 commit comments

Comments
 (0)