Skip to content

Commit 04ae85a

Browse files
committed
Integration of chatgpt on request for comment and score
1 parent d106348 commit 04ae85a

File tree

21 files changed

+557
-2
lines changed

21 files changed

+557
-2
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ gem 'tubesock', github: 'openhpi/tubesock'
5656
gem 'turbolinks'
5757
gem 'whenever', require: false
5858
gem 'zxcvbn-ruby', require: 'zxcvbn'
59+
gem 'openai'
60+
5961

6062
# Error Tracing
6163
gem 'mnemosyne-ruby'

app/assets/javascripts/editor.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,50 @@ $(document).on('turbolinks:load', function(event) {
2929

3030
$(document).on('theme:change', handleThemeChangeEvent.bind(this));
3131
});
32+
33+
$(document).on('click', '.ai-feedback-link', function (e) {
34+
e.preventDefault();
35+
36+
const testrunId = $(this).data('testrun_id');
37+
const $button = $(this);
38+
39+
if (!testrunId) {
40+
alert("No Testrun ID available for this feedback.");
41+
return;
42+
}
43+
44+
$.ajax({
45+
url: `/submissions/testrun_ai_feedback`,
46+
type: 'POST',
47+
data: { testrun_id: testrunId },
48+
beforeSend: function () {
49+
$button.html(
50+
'<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Loading...</span></div>'
51+
);
52+
$button.prop('disabled', true);
53+
},
54+
success: function (response) {
55+
// Find the card containing the button
56+
const card = $button.closest('.card');
57+
58+
if (card.length) {
59+
// Update the feedback message in the card
60+
card.find('.row .col-md-9').eq(2).html(response);
61+
62+
// Hide or remove the button after feedback is displayed
63+
$button.remove();
64+
} else {
65+
console.error("Card not found for testrun_id:", testrunId);
66+
}
67+
},
68+
error: function (xhr) {
69+
alert(`Failed to fetch feedback: ${xhr.responseText}`);
70+
// Re-enable the button in case of an error
71+
$button.html("Request Feedback from AI");
72+
$button.prop('disabled', false);
73+
},
74+
complete: function () {
75+
// No action needed here since button is removed on success
76+
}
77+
});
78+
});

app/assets/javascripts/editor/editor.js.erb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ var CodeOceanEditor = {
547547
card.find('.card-title .number').text(index + 1);
548548
card.find('.row .col-md-9').eq(0).find('.number').eq(0).text(result.passed);
549549
card.find('.row .col-md-9').eq(0).find('.number').eq(1).text(result.count);
550+
card.attr('data-testrun-id', result.testrun_id); // Add testrun_id to the card
550551
if (result.weight !== 0) {
551552
card.find('.row .col-md-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
552553
card.find('.row .col-md-9').eq(1).find('.number').eq(1).text(result.weight);
@@ -555,6 +556,10 @@ var CodeOceanEditor = {
555556
card.find('.attribute-row.row').eq(1).addClass('d-none');
556557
}
557558
card.find('.row .col-md-9').eq(2).html(result.message);
559+
if (result.testrun_id) {
560+
const aiFeedbackLink = card.find('.ai-feedback-link');
561+
aiFeedbackLink.data('testrun_id', result.testrun_id);
562+
}
558563

559564
// Add error message from code to card
560565
if (result.error_messages) {

app/controllers/exercises_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ def exercise_params
209209
:hide_file_tree,
210210
:allow_file_creation,
211211
:allow_auto_completion,
212+
:allow_ai_comment_for_rfc,
213+
:allow_ai_feedback_on_score,
212214
:title,
213215
:internal_title,
214216
:expected_difficulty,

app/controllers/request_for_comments_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ def create
154154
format.json { render json: {danger: t('exercises.editor.depleted'), status: :container_depleted}, status: :service_unavailable }
155155
else
156156
format.json { render :show, status: :created, location: @request_for_comment }
157+
if @request_for_comment.submission.exercise.allow_ai_comment_for_rfc
158+
GenerateAutomaticCommentsJob.perform_later(@request_for_comment, current_user)
159+
end
157160
end
158161
else
159162
format.html { render :new }

app/controllers/submissions_controller.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class SubmissionsController < ApplicationController
2121
before_action :require_user!, except: :render_file
2222
# We want to serve .js files without raising a `ActionController::InvalidCrossOriginRequest` exception
2323
skip_before_action :verify_authenticity_token, only: %i[render_file download_file]
24+
skip_after_action :verify_authorized, only: :testrun_ai_feedback_message
2425

2526
def index
2627
@search = policy_scope(Submission).ransack(params[:q])
@@ -332,6 +333,19 @@ def test
332333
kill_client_socket(client_socket)
333334
end
334335

336+
def testrun_ai_feedback_message
337+
testrun = Testrun.find(params[:testrun_id])
338+
339+
# Use the generate_feedback method from the Testrun model
340+
feedback_message = testrun.generate_ai_feedback
341+
342+
if feedback_message.present?
343+
render plain: feedback_message, status: :ok
344+
else
345+
render plain: 'Failed to generate feedback.', status: :unprocessable_entity
346+
end
347+
end
348+
335349
private
336350

337351
def authorize!
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# app/jobs/generate_automatic_comments_job.rb
2+
class GenerateAutomaticCommentsJob < ApplicationJob
3+
queue_as :default
4+
def perform(request_for_comment, current_user)
5+
chat_gpt_user = InternalUser.find_by(email: '[email protected]')
6+
chat_gpt_service = ChatGptService::ChatGptRequest.new
7+
chat_gpt_disclaimer = I18n.t('exercises.editor.chat_gpt_disclaimer')
8+
request_for_comment.submission.files.each do |file|
9+
response_data = chat_gpt_service.get_response(
10+
request_for_comment: request_for_comment,
11+
file: file,
12+
response_format_needed: true
13+
)
14+
Rails.logger.debug "Response data: #{response_data.inspect}"
15+
next unless response_data.present?
16+
17+
# Create comment for combined 'requirements' comments
18+
if response_data[:requirements_comments].present?
19+
Rails.logger.debug "Requirements comments found."
20+
comment = create_comment(
21+
text: "#{response_data[:requirements_comments]}\n\n#{chat_gpt_disclaimer}",
22+
file_id: file.id,
23+
row: '0',
24+
column: '0',
25+
user: chat_gpt_user
26+
)
27+
send_emails(comment, request_for_comment, current_user, chat_gpt_user) if comment.persisted?
28+
end
29+
30+
# Create comments for each line-specific comment
31+
response_data[:line_specific_comments].each do |line_comment_data|
32+
create_comment(
33+
text: "#{line_comment_data[:comment]}\n\n#{chat_gpt_disclaimer}",
34+
file_id: file.id,
35+
row: line_comment_data[:line_number].to_s,
36+
column: '0',
37+
user: chat_gpt_user
38+
)
39+
end
40+
end
41+
end
42+
43+
private
44+
45+
def create_comment(attributes)
46+
Comment.create(
47+
text: attributes[:text],
48+
file_id: attributes[:file_id],
49+
row: attributes[:row],
50+
column: attributes[:column],
51+
user: attributes[:user]
52+
)
53+
end
54+
55+
def send_emails(comment, request_for_comment, current_user, chat_gpt_user)
56+
send_mail_to_author(comment, request_for_comment, chat_gpt_user)
57+
send_mail_to_subscribers(comment, request_for_comment, current_user)
58+
end
59+
60+
def send_mail_to_author(comment, request_for_comment, chat_gpt_user)
61+
if chat_gpt_user == comment.user
62+
UserMailer.got_new_comment(comment, request_for_comment, chat_gpt_user).deliver_later
63+
end
64+
end
65+
66+
def send_mail_to_subscribers(comment, request_for_comment, current_user)
67+
request_for_comment.commenters.each do |commenter|
68+
subscriptions = Subscription.where(
69+
request_for_comment_id: request_for_comment.id,
70+
user: commenter,
71+
deleted: false
72+
)
73+
subscriptions.each do |subscription|
74+
next if subscription.user == current_user
75+
76+
should_send = (subscription.subscription_type == 'author' && current_user == request_for_comment.user) ||
77+
(subscription.subscription_type == 'all')
78+
79+
if should_send
80+
UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_later
81+
break
82+
end
83+
end
84+
end
85+
end
86+
end

app/models/submission.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def score_file(output, file, requesting_user)
317317
end
318318

319319
output.merge!(assessment)
320-
output.merge!(filename:, message: feedback_message(file, output), weight: file.weight, hidden_feedback: file.hidden_feedback)
320+
output.merge!(filename:, message: feedback_message(file, output), weight: file.weight, hidden_feedback: file.hidden_feedback, testrun_id: testrun.id)
321321
output.except!(:messages)
322322
end
323323

app/models/testrun.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,40 @@ def log
3131
testrun_messages.output.pluck(:log).join.presence
3232
end
3333
end
34+
35+
def generate_ai_feedback
36+
# Validate if the exercise allows automatic feedback
37+
unless submission.exercise.allow_ai_feedback_on_score
38+
raise 'Automatic feedback is not enabled for this exercise.'
39+
end
40+
41+
# Generate feedback without storing it
42+
main_file = submission.main_file
43+
exercise_description = submission.exercise.description
44+
test_results = output
45+
learner_solution = main_file.content
46+
test_passed = passed
47+
48+
chatgpt_request = ChatGptService::ChatGptRequest.new
49+
feedback_message = chatgpt_request.get_response(
50+
learner_solution: learner_solution,
51+
exercise: exercise_description,
52+
test_results: test_results,
53+
test_passed: test_passed
54+
)
55+
56+
# Format and sanitize the feedback message
57+
formatted_feedback = Kramdown::Document.new(feedback_message).to_html
58+
sanitized_feedback = ActionController::Base.helpers.sanitize(
59+
formatted_feedback,
60+
tags: %w(p br strong em a code pre h1 h2 h3 ul ol li blockquote),
61+
attributes: %w(href)
62+
)
63+
64+
sanitized_feedback.html_safe
65+
rescue => e
66+
# Return the error message to be handled by the caller
67+
e.message
68+
end
69+
3470
end
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<prompt>
2+
<case>
3+
1-1 Syntaxfehler-Anfänger
4+
</case>
5+
<role>
6+
Du agierst als Tutor für einen Lernenden, der ein Problem bei einer Programmieraufgabe hat. Antworte im Folgenden entsprechend deiner Rolle als Tutor!
7+
</role>
8+
<instructions>
9+
<instruction_1>
10+
Deine Aufgabe ist es, einen Hinweis für den in """ abgegrenzten Code des Lernenden zu formulieren.
11+
</instruction_1>
12+
<instruction_2>
13+
Der von dir generierte Hinweis muss jede der in ### abgegrenzten Anforderungen erfüllen.
14+
</instruction_2>
15+
<instruction_3>
16+
Der von dir generierte Hinweis soll, um die Anforderungen mit den Nummern 7, 8 und 9 zu erfüllen, unbedingt die Aufgabenstellung der Programmieraufgabe, die in %%% abgegrenzt ist, berücksichtigen.
17+
</instruction_3>
18+
<instruction_4>
19+
Der von dir generierte Hinweis soll, um die Anforderung mit der Nummer 10 zu erfüllen, unbedingt die Fehlermeldung, die der Code hervorgerufen hat, berücksichtigen. Diese Fehlermeldung liegt abgegrenzt in $$$ vor. Wenn keine Fehlermeldung angezeigt wird, bedeutet dies, dass der Testfall bestanden wurde.
20+
</instruction_4>
21+
<instruction_5>
22+
Die Anforderungen mit den Nummern 6, 7, 8, 9, 10 und 11 sollen jeweils in einem eigenen Absatz getrennt voneinander im Hinweis erfüllt werden.
23+
</instruction_5>
24+
<instruction_6>
25+
Gehe systematisch vor! Reflektiere deine Vorgehensweise! Gib jedoch nur den Hinweis und nicht deine Vorgehensweise aus!
26+
</instruction_6>
27+
<instruction_7>Der von dir generierte Hinweis soll die Frage beantworten, wenn der Studierende gefragt hat. Die Frage wird durch ??? erfüllt.</instruction_7>
28+
</instructions>
29+
<requirements>
30+
<delimiter>###</delimiter>
31+
<requirement_1>1. Der Hinweis muss einen Fehler im Code identifizieren und benennen.</requirement_1>
32+
<requirement_2>2. Der Hinweis darf keine nichtexistenten Fehler identifizieren.</requirement_2>
33+
<requirement_3>3. Der Hinweis darf keine, nicht den Fehler betreffenden Inhalte enthalten.</requirement_3>
34+
<requirement_4>4. Der Hinweis darf keinen Code, welcher als Musterlösung verstanden werden kann, enthalten.</requirement_4>
35+
<requirement_5>5. Der Hinweis darf keine Test-Cases enthalten.</requirement_5>
36+
<requirement_KCR-feedback>6. Der Hinweis muss eine Beschreibung oder Andeutung der richtigen Lösung enthalten. Dieser Bestandteil soll in deinem Hinweis mit “(1)” gekennzeichnet sein. Hierin soll eine Beschreibung vorliegen, dass in einer bestimmten Stelle im Code etwas stehen sollte – ohne dass ein Code-Beispiel ausgegeben wird. Dein Hinweis hierzu muss sich unbedingt auf die Problemstelle im Code beziehen! Beschreibe nicht den vorliegenden Fehler, sondern die richtige Lösung!</requirement_KCR-feedback>
37+
<requirement_KTC-TR-feedback>7. Der Hinweis muss Regeln, Einschränkungen oder Anforderungen der Aufgabenstellung enthalten – in Form von Hinweisen zu Anforderungen der Aufgabenstellung. Z. B. ist eine Anforderung, dass eine bestimmte vordefinierte Methode benutzt werden soll oder eine Methode einer bestimmten Programmbibliothek nicht benutzt werden soll. Dieser Bestandteil soll in deinem Hinweis mit “(2)” gekennzeichnet sein. Nenne nur Anforderungen aus der Aufgabenstellung, die sich direkt auf den Fehler im Code beziehen! Nenne keine Anforderungen, die nicht relevant für den Fehler im Code sind!</requirement_KTC-TR-feedback>
38+
<requirement_KC-EXP-feedback>8. Der Hinweis muss konzeptuelles Wissen, welches für die Bearbeitung der Aufgabe benötigt wird, enthalten – in Form von Erläuterungen von grundlegenden Konzepten der Aufgabe. Dieser Bestandteil soll in deinem Hinweis mit “(3)” gekennzeichnet sein.</requirement_KC-EXP-feedback>
39+
<requirement_KC-EXA-feedback>9. Der Hinweis muss konzeptuelles Wissen, welches für die Bearbeitung der Aufgabe benötigt wird, enthalten – in Form von Beispielen, welche grundlegende Konzepte der Aufgabe verdeutlichen. Dieser Bestandteil soll in deinem Hinweis mit “(4)” gekennzeichnet sein. Beziehe dich auf das von dir ausgewählte Konzept in der Antwort auf die Anforderung Nummer 8. Finde hierfür ein eigenes, konkretes Beispiel, welches das grundlegende Konzept implementiert und sich nicht auf die vorliegende Programmieraufgabe bezieht! Zeige unbedingt einen Code-Schnipsel, um dein Beispiel zu untermauern!</requirement_KC-EXA-feedback>
40+
<requirement_KM-feedback>10. Der Hinweis muss Compiler-Fehler bzw. Interpreter-Fehler beschreiben. Compiler-Fehler bzw. Interpreter-Fehler sind syntaktische Fehler (falsche Schreibweise, fehlende Klammern). Dieser Bestandteil soll in deinem Hinweis mit “(5)” gekennzeichnet sein. Es soll eine ausführliche Erklärung des Fehlers an sich gegeben werden. Hierin sollen keine Schritte zur Lösung des Fehlers ausgegeben werden.</requirement_KM-feedback>
41+
<requirement_KH-EC-feedback>11. Der Hinweis muss Wissen über das weitere Vorgehen des Lernenden enthalten. Beziehe dich auf die Behebung der Art des vorliegenden Fehlers! Dieser Bestandteil soll in deinem Hinweis mit “(6)” gekennzeichnet sein. Erkläre allgemein den methodischen Schritt zur Behebung des Fehlers. Zeige hierzu unter keinen Umständen die exakte Vorgehensweise zum Lösen des Fehlers – z. B. „Ändere dies in der Zeile X“! Zeige stattdessen, welche generellen Schritte der Lernende als Nächstes unternehmen kann. Z. B. kann der Hinweis lauten: „Als Nächstes recherchiere dieses Thema im Internet.“, „Informiere dich über diese Thematik“, „Passe deinen Code in dieser Hinsicht an.“ oder „Teste die Ausgabe deines Codes.“</requirement_KH-EC-feedback>
42+
</requirements>
43+
<input>
44+
<code name="Code des Lernenden" type="string">
45+
<delimiter>"""</delimiter>
46+
<description>Code des Lernenden:</description>
47+
<content>[Code des Lernenden]</content>
48+
</code>
49+
<programming_task name="Aufgabenstellung" type="string">
50+
<delimiter>%%%</delimiter>
51+
<description>Aufgabenstellung der Programmieraufgabe:</description>
52+
<content>[Aufgabenstellung]</content>
53+
</programming_task>
54+
<error_message name="Fehlermeldung" type="string">
55+
<delimiter>$$$</delimiter>
56+
<description>Fehlermeldung, die der Code hervorgerufen hat:</description>
57+
<content>[Fehlermeldung]</content>
58+
</error_message>
59+
<student_question name="Frage des Studierenden" type="string">
60+
<delimiter>???</delimiter>
61+
<description>Frage des Studierenden</description>
62+
<content>[Frage des Studierenden]</content>
63+
</student_question>
64+
</input>
65+
</prompt>

0 commit comments

Comments
 (0)