@@ -7,9 +7,11 @@ permissions:
77 checks : write
88 actions : read
99 contents : read
10+ pull-requests : write
1011
1112jobs :
1213 run-autograding-tests :
14+ name : Autograding
1315 runs-on : ubuntu-latest
1416 if : github.actor != 'github-classroom[bot]'
1517 steps :
@@ -57,3 +59,175 @@ jobs:
5759 EDGE-CASE-TESTS_RESULTS : " ${{steps.edge-case-tests.outputs.result}}"
5860 with :
5961 runners : compilation-check,basic-tests,edge-case-tests
62+ ai_feedback :
63+ name : AI-Powered Feedback
64+ needs : autograding
65+ if : ${{ needs.autograding.result == 'success' }}
66+ runs-on : ubuntu-latest
67+ permissions :
68+ pull-requests : write
69+ env :
70+ OPENROUTER_MODEL : ${{ vars.OPENROUTER_MODEL }}
71+ SYSTEM_PROMPT : ${{ vars.SYSTEM_PROMPT }}
72+ steps :
73+ - name : Checkout repository
74+ uses : actions/checkout@v5
75+
76+ - name : Read assignment instructions
77+ id : instructions
78+ run : |
79+ # Reads the content of the README.md file into an output variable.
80+ # The `EOF` marker is used to handle multi-line file content.
81+ echo "instructions=$(cat README.md | sed 's/\"/\\\"/g' | sed 's/$/\\n/' | tr -d '\n' | sed 's/\\n/\\\\n/g')" >> $GITHUB_OUTPUT
82+
83+ - name : Read source code
84+ id : source_code
85+ run : |
86+ {
87+ echo 'source_code<<EOF'
88+ find src/main/java -type f -name "*.java" | while read -r file; do
89+ echo "=== File: $file ==="
90+ cat "$file"
91+ echo
92+ done
93+ echo 'EOF'
94+ } >> "$GITHUB_OUTPUT"
95+
96+ - name : Read test code
97+ id : test_code
98+ run : |
99+ {
100+ echo 'test_code<<EOF'
101+ if [ -d "src/test/java" ]; then
102+ find src/test/java -type f -name "*.java" | while read -r file; do
103+ echo "=== File: $file ==="
104+ cat "$file"
105+ echo
106+ done
107+ else
108+ echo "No test code found."
109+ fi
110+ echo 'EOF'
111+ } >> "$GITHUB_OUTPUT"
112+ - name : Generate AI Feedback
113+ id : ai_feedback
114+ run : |
115+ # This step sends the collected data to the OpenRouter API.
116+ INSTRUCTIONS=$(jq -Rs . <<'EOF'
117+ ${{ steps.instructions.outputs.instructions }}
118+ EOF
119+ )
120+ SOURCE_CODE=$(jq -Rs . <<'EOF'
121+ ${{ steps.source_code.outputs.source_code }}
122+ EOF
123+ )
124+ TEST_CODE=$(jq -Rs . <<'EOF'
125+ ${{ steps.test_code.outputs.test_code }}
126+ EOF
127+ )
128+
129+ if [ -z "$INSTRUCTIONS" ] || [ -z "$SOURCE_CODE" ] || [ -z "$TEST_CODE" ]; then
130+ echo "Error: One or more required variables are not set."
131+ exit 1
132+ fi
133+
134+ # Assigning to USER_CONTENT with variable expansion
135+ PAYLOAD="Please provide feedback on the following Java assignment.
136+
137+ --- Assignment Instructions ---
138+ ${INSTRUCTIONS}
139+
140+ --- Source files ---
141+ ${SOURCE_CODE}
142+
143+ --- Test files ---
144+ ${TEST_CODE}"
145+
146+ JSON_CONTENT=$(jq -n \
147+ --argjson model "$OPENROUTER_MODEL" \
148+ --arg system_prompt "$SYSTEM_PROMPT" \
149+ --arg payload "$PAYLOAD" \
150+ '{
151+ models: $model,
152+ messages: [
153+ {role: "system", content: $system_prompt},
154+ {role: "user", content: $payload}
155+ ]
156+ }')
157+
158+ echo "$JSON_CONTENT"
159+
160+ API_RESPONSE=$(echo "$JSON_CONTENT" | curl https://openrouter.ai/api/v1/chat/completions \
161+ -H "Authorization: Bearer ${{ secrets.OPENROUTER_API_KEY }}" \
162+ -H "Content-Type: application/json" \
163+ -d @-)
164+
165+ echo "$API_RESPONSE"
166+
167+ FEEDBACK_CONTENT=$(echo "$API_RESPONSE" | jq -r '.choices[0].message.content')
168+ echo "feedback<<EOF" >> $GITHUB_OUTPUT
169+ echo "$FEEDBACK_CONTENT" >> $GITHUB_OUTPUT
170+ echo "EOF" >> $GITHUB_OUTPUT
171+ - name : Post Feedback as PR Comment ✍️
172+ uses : actions/github-script@v7
173+ env :
174+ FEEDBACK_BODY : ${{ steps.ai_feedback.outputs.feedback }}
175+ with :
176+ github-token : ${{ secrets.GITHUB_TOKEN }}
177+ script : |
178+ const { owner, repo } = context.repo;
179+ const targetTitle = "Feedback";
180+ const signature = "🤖 AI Feedback";
181+
182+ const { data: pullRequests } = await github.rest.pulls.list({
183+ owner,
184+ repo,
185+ state: "open",
186+ per_page: 100
187+ });
188+
189+ const matchingPR = pullRequests.find(pr => pr.title.trim().toLowerCase() === targetTitle.toLowerCase());
190+ if (!matchingPR) {
191+ throw new Error(`No open pull request found with title '${targetTitle}'`);
192+ }
193+
194+ const prNumber = matchingPR.number;
195+
196+ const { data: comments } = await github.rest.issues.listComments({
197+ owner,
198+ repo,
199+ issue_number: prNumber,
200+ per_page: 100
201+ });
202+
203+ const existing = comments.find(c =>
204+ c.user?.login === "github-actions[bot]" &&
205+ c.body?.includes(signature)
206+ );
207+
208+ const timestamp = new Date().toISOString();
209+ const newEntry = `🕒 _Posted on ${timestamp}_\n\n${process.env.FEEDBACK_BODY}\n\n---\n`;
210+
211+ if (existing) {
212+ // Extract previous entries and wrap them in a collapsible block
213+ const previousContent = existing.body.replace(/^### 🤖 AI Feedback\s*/, '').trim();
214+ const collapsed = `<details><summary>Previous Feedback</summary>\n\n${previousContent}\n</details>`;
215+
216+ const updatedBody = `### ${signature}\n\n${newEntry}${collapsed}`;
217+ await github.rest.issues.updateComment({
218+ owner,
219+ repo,
220+ comment_id: existing.id,
221+ body: updatedBody
222+ });
223+ console.log(`🔄 Updated existing comment on PR #${prNumber}`);
224+ } else {
225+ const body = `### ${signature}\n\n${newEntry}`;
226+ await github.rest.issues.createComment({
227+ owner,
228+ repo,
229+ issue_number: prNumber,
230+ body
231+ });
232+ console.log(`🆕 Posted new comment on PR #${prNumber}`);
233+ }
0 commit comments