Files
euri-iso/.github/workflows/ai-moderator.yml
T

203 lines
7.5 KiB
YAML

name: AI Moderator
on:
issues:
types: [opened]
pull_request:
types: [opened]
discussion:
types: [created]
permissions:
issues: write
pull-requests: write
discussions: write
contents: read
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v9
env:
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
with:
script: |
const fs = require('fs');
const coc = (() => { try { return fs.readFileSync('.github/CODE_OF_CONDUCT.md', 'utf8'); } catch (e) { return ''; } })();
const contributing = (() => { try { return fs.readFileSync('CONTRIBUTING.md', 'utf8'); } catch (e) { return ''; } })();
const content = context.payload.issue?.body
|| context.payload.pull_request?.body
|| context.payload.discussion?.body
|| '';
if (!content.trim()) {
console.log('No content to analyze');
return;
}
const actor = context.payload.sender?.login;
if (actor === 'c-ludenberg') {
console.log('Repo owner, skipping moderation');
return;
}
const isDiscussion = context.payload.discussion !== undefined;
const prompt = [
'You are an AI moderator for the Antergos NeXT project, an Artix-based',
'Linux distribution (dinit init, KDE Plasma).',
'Speak in first person. Be direct, confident, and have some snark.',
'The project has a playful but no-nonsense tone - match it.',
'',
'Your job is NOT to police opinions, bug reports, feature requests,',
'pull requests, technical questions, or constructive criticism.',
'Those are all normal and welcome. A normal PR description or issue',
'is NEVER a violation, no matter how long, detailed, or assertive it is.',
'',
'ONLY flag content that genuinely violates the Code of Conduct:',
'harassment, personal attacks, hate speech, slurs, spam, scams,',
'doxxing, malicious links, or threats.',
'',
'When in doubt, or if the content is merely technical, opinionated,',
'or critical of the project - violation is FALSE.',
'',
'If the content says it is a test or contains phrases like moderator',
'or test or reply here, play along with the joke but still enforce the rules.',
'',
'Code of Conduct:',
coc,
'',
'Contributing Guidelines:',
contributing,
'',
'Analyze the content below for violations.',
'Respond with ONLY valid JSON - no markdown, no backticks.',
'',
'{',
' "violation": true/false,',
' "message": "your direct first-person response",',
' "action": "warn",',
' "severity": 1-10',
'}',
'',
'"action" options: "warn" (post warning), "hide" (minimize comment), "lock" (lock issue/discussion)',
'For issues use "warn" or "lock". For comments you may use "hide".',
'Only use "lock" or "hide" for severe violations.',
'',
'Content:',
content
].join('\n');
const model = 'llama-3.1-8b-instant';
let decision;
try {
const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.GROQ_API_KEY}`
},
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: 'You are a helpful community moderator. Respond only with valid JSON as instructed.' },
{ role: 'user', content: prompt }
],
temperature: 0.1,
max_tokens: 250
})
});
const data = await response.json();
const raw = data.choices?.[0]?.message?.content || '{}';
decision = JSON.parse(raw.replace(/```json|```/g, '').trim());
console.log(`Decision: ${JSON.stringify(decision)}`);
} catch (err) {
console.log(`API error: ${err}`);
return;
}
if (!decision.violation) {
console.log('Content deemed acceptable');
return;
}
if (typeof decision.severity === 'number' && decision.severity < 4) {
console.log(`Low severity (${decision.severity}), no action taken`);
return;
}
const number = context.payload.issue?.number
|| context.payload.pull_request?.number
|| context.payload.discussion?.number;
if (!number) {
console.log('No issue/PR/discussion number found');
return;
}
const owner = context.repo.owner;
const repo = context.repo.repo;
try {
if (isDiscussion) {
const discId = context.payload.discussion.node_id;
if (discId) {
const msg = decision.message.replace(/"/g, '\\"');
await github.graphql(`
mutation {
addDiscussionComment(input: {discussionId: "${discId}", body: "${msg}"}) {
comment { id }
}
}
`);
}
} else {
await github.rest.issues.addLabels({
owner, repo, issue_number: number, labels: ['ai-moderated']
});
await github.rest.issues.createComment({
owner, repo, issue_number: number, body: decision.message
});
}
if (decision.action === 'hide') {
const commentId = context.payload.comment?.node_id;
if (commentId) {
await github.graphql(`
mutation {
minimizeComment(input: {subjectId: "${commentId}", classifier: OFF_TOPIC}) {
minimizedComment { isMinimized }
}
}
`);
console.log('Comment minimized');
}
}
if (decision.action === 'lock') {
if (isDiscussion) {
const discId = context.payload.discussion.node_id;
if (discId) {
await github.graphql(`
mutation {
lockLockable(input: {lockableId: "${discId}"}) {
lockedRecord { activeLockReason }
}
}
`);
}
} else {
await github.rest.issues.lock({ owner, repo, issue_number: number, lock_reason: 'too_heated' });
}
console.log('Locked');
}
console.log('Action completed: ' + decision.action);
} catch (err) {
console.log(`GitHub API error: ${err}`);
}