Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions .github/triage-issues-agentic.lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Issue Triage Agent (Agentic)

on:
issues:
types: [opened, edited]

schedule:
- cron: '0 */6 * * *'

workflow_dispatch:
inputs:
mode:
description: 'Run mode: single or all'
required: true
default: 'all'
type: choice
options:
- all
- single
force:
description: 'Re-triage issues that already have a triage label (debug only)'
required: false
default: false
type: boolean

permissions:
contents: read
issues: write
pull-requests: write
models: read # required for the GitHub Models inference API

jobs:
triage:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
pip install -r requirements.txt || true
gh --version

# ─────────────────────────────────────────────────────────────
# SINGLE-ISSUE PATH (triggered by issue opened/edited)
#
# We do NOT inline ${{ github.event.issue.body }} into a heredoc —
# backticks, quotes, and $-signs in the body would otherwise mangle
# the script. Instead we fetch the issue via `gh api` so it round-trips
# cleanly through JSON.
# ─────────────────────────────────────────────────────────────
- name: Fetch issue payload (single)
if: github.event_name == 'issues'
run: |
echo "📥 Fetching issue #${{ github.event.issue.number }} via gh api"
gh api "repos/${GITHUB_REPOSITORY}/issues/${{ github.event.issue.number }}" \
--jq '{number: .number, title: .title, body: .body}' \
> /tmp/issue.json
echo "── Issue payload (sanitized preview) ──"
jq '{number, title, body_length: (.body | length)}' /tmp/issue.json
# Wrap into a list so triage_batch.py can consume it uniformly
jq '[.]' /tmp/issue.json > /tmp/issues.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Log full body (single)
if: github.event_name == 'issues'
run: |
echo "================================================================================"
echo "� FULL ISSUE BODY (as the AI will see it)"
echo "================================================================================"
jq -r '.body' /tmp/issue.json
echo "================================================================================"

- name: Pre-label as triaged (single)
if: github.event_name == 'issues'
run: |
gh label create "triaged" --repo "$GITHUB_REPOSITORY" 2>/dev/null || true
gh issue edit ${{ github.event.issue.number }} \
--add-label "triaged" --repo "$GITHUB_REPOSITORY"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true

# ─────────────────────────────────────────────────────────────
# AI-DRIVEN TRIAGE — the LLM reads the body and decides everything:
# classification, the comment to post, and (for auto_fix) the literal
# current_text / desired_text to substitute in the docs.
# triage_batch.py applies the AI's decision (label + comment + PR).
# ─────────────────────────────────────────────────────────────
- name: AI triage + (optional) auto-PR (single)
if: github.event_name == 'issues'
run: |
echo "🤖 Running AI triage via GitHub Models on issue #${{ github.event.issue.number }}"
python3 triage_batch.py /tmp/issues.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LEARN_PR_TOKEN: ${{ secrets.LEARN_PR_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
AI_MODEL: openai/gpt-4o-mini
# Set AI_DEBUG=1 to dump the full prompt + raw model response.
AI_DEBUG: '1'
# Single-issue (event-driven) runs always reprocess — a human
# just opened/edited the issue and wants fresh triage.
TRIAGE_MODE: single

# ─────────────────────────────────────────────────────────────
# BATCH PATH (scheduled OR workflow_dispatch with mode=all)
# ─────────────────────────────────────────────────────────────
- name: Batch triage (scheduled / manual)
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'all')
run: |
echo "🔄 BATCH TRIAGE starting..."
# We include `labels` in the JSON so triage_batch.py can skip
# issues we've already handled (anything carrying one of the
# TRIAGE_LABELS — triaged / auto-fix / needs-review / etc.).
gh issue list --state open \
--json number,title,body,labels --limit 50 \
--repo "$GITHUB_REPOSITORY" > /tmp/issues.json
echo "📋 Fetched $(jq length /tmp/issues.json) open issues"
python3 triage_batch.py /tmp/issues.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LEARN_PR_TOKEN: ${{ secrets.LEARN_PR_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
AI_MODEL: openai/gpt-4o-mini
TRIAGE_MODE: batch
# Only re-triage already-labeled issues when the user explicitly
# asks for it via the workflow_dispatch "force" input.
FORCE_RETRIAGE: ${{ github.event.inputs.force == 'true' && '1' || '' }}
58 changes: 58 additions & 0 deletions _smoke_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Smoke-test the AI triage pipeline.

Locally there is no GITHUB_TOKEN with models:read scope, so the AI call
raises and we exercise the heuristic fallback path. The point of this test
is to prove the fallback returns sensible classifications + comments.
"""
import os
import sys

os.environ.pop("GITHUB_TOKEN", None) # force the fallback path

from ai_triage import ai_or_fallback

ISSUES = [
{
"number": 7,
"title": "MS Learn Module Update Request: [REPLACE_WITH_MODULE_TITLE]",
"body": (
"Which of the MS Learn modules from the dropdown are you submitting an update request?\n"
"None\n\nAdditional information\n\n"
"Fix a broken user experience (broken links, exercise error, etc.)\n\n"
"Update incorrect information\n\nAdd new content to the module\n\n"
"Some other request\nInformation about the requested update\nNo response"
),
"expected": "spam",
},
{
"number": 4,
"title": "MS Learn Module Update Request: Manage your work with GitHub Projects",
"body": (
"Information about the requested update\n"
"https://learn.microsoft.com/en-us/training/modules/manage-work-github-projects/7-knowledge-check\n\n"
"In Module assessment/Check your knowledge\n\n"
"What Project descriptor automatically saves when you change it?\n\n"
"Project name (Correct)\n\n\nProject description\n\n\nProject README\n\n"
"first answer is marked as correct, should be: None of them."
),
"expected": "auto_fix",
},
]

lines = []
for issue in ISSUES:
r = ai_or_fallback(issue)
lines.append(
f"#{issue['number']:>3} expected={issue['expected']:<13} "
f"got={r['classification']:<13} ({r['source']}, conf={r['confidence']})"
)
lines.append(f" comment[:140] = {r['comment'][:140]!r}")
if r.get("fix_plan"):
fp = r["fix_plan"]
lines.append(f" fix_plan = current={fp.get('current_text')!r} desired={fp.get('desired_text')!r}")

text = "\n".join(lines) + "\n"
sys.stdout.write(text)
sys.stdout.flush()
with open("_smoke_result.txt", "w") as fh:
fh.write(text)
Loading