• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / backend / 9c2823cf899c9314fd4ccec9dd6c3b589d83e839

04 Dec 2025 05:47PM UTC coverage: 88.716% (-0.9%) from 89.621%
9c2823cf899c9314fd4ccec9dd6c3b589d83e839

push

github

web-flow
AI-powered marking (#1248)

* feat: v1 of AI-generated comments

* feat: added logging of inputs and outputs

* Update generate_ai_comments.ex

* feat: function to save outputs to database

* Format answers json before sending to LLM

* Add LLM Prompt to question params when submitting assessment xml file

* Add LLM Prompt to api response when grading view is open

* feat: added llm_prompt from qn to raw_prompt

* feat: enabling/disabling of LLM feature by course level

* feat: added llm_grading boolean field to course creation API

* feat: added api key storage in courses & edit api key/enable llm grading

* feat: encryption for llm_api_key

* feat: added final comment editing route

* feat: added logging of chosen comments

* fix: bugs when certain fields were missing

* feat: updated tests

* formatting

* fix: error handling when calling openai API

* fix: credo issues

* formatting

* Address some comments

* Fix formatting

* rm IO.inspect

* a

* Use case instead of if

* Streamlines generate_ai_comments to only send the selected question and its relevant info + use the correct llm_prompt

* Remove unncessary field

* default: false for llm_grading

* Add proper linking between ai_comments table and submissions. Return it to submission retrieval as well

* Resolve some migration comments

* Add llm_model and llm_api_url to the DB + schema

* Moves api key, api url, llm model and course prompt to course level

* Add encryption_key to env

* Do not hardcode formatting instructions

* Add Assessment level prompts to the XML

* Return some additional info for composing of prompts

* Remove un-used 'save comments'

* Fix existing assessment tests

* Fix generate_ai_comments test cases

* Fix bug preventing avengers from generating ai comments

* Fix up tests + error msgs

* Formatting

* some mix credo suggestions

* format

* Fix credo issue

* bug fix + credo fixes

* Fix tests

* format

* Modify test.exs

* Update lib/cadet_web/controllers/gener... (continued)

118 of 174 new or added lines in 9 files covered. (67.82%)

1 existing line in 1 file now uncovered.

3758 of 4236 relevant lines covered (88.72%)

7103.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.7
/lib/cadet_web/admin_views/admin_grading_view.ex
1
defmodule CadetWeb.AdminGradingView do
2
  use CadetWeb, :view
3

4
  import CadetWeb.AssessmentsHelpers
5
  alias CadetWeb.AICodeAnalysisController
6

7
  def render("show.json", %{course: course, answers: answers, assessment: assessment}) do
8
    %{
2✔
9
      assessment:
10
        render_one(assessment, CadetWeb.AdminGradingView, "assessment.json", as: :assessment),
11
      answers:
12
        render_many(answers, CadetWeb.AdminGradingView, "grading_info.json",
13
          as: :answer,
14
          course: course,
15
          assessment: assessment
16
        ),
17
      enable_llm_grading: course.enable_llm_grading
2✔
18
    }
19
  end
20

21
  def render("assessment.json", %{assessment: assessment}) do
22
    %{
2✔
23
      id: assessment.id,
2✔
24
      title: assessment.title,
2✔
25
      summaryShort: assessment.summary_short,
2✔
26
      summaryLong: assessment.summary_long,
2✔
27
      coverPicture: assessment.cover_picture,
2✔
28
      number: assessment.number,
2✔
29
      story: assessment.story,
2✔
30
      reading: assessment.reading
2✔
31
    }
32
  end
33

34
  def render("gradingsummaries.json", %{
35
        count: count,
36
        data: %{
37
          users: users,
38
          assessments: assessments,
39
          submissions: submissions,
40
          teams: teams,
41
          team_members: team_members
42
        }
43
      }) do
44
    %{
5✔
45
      count: count,
46
      data:
47
        for submission <- submissions do
48
          user = users |> Enum.find(&(&1.id == submission.student_id))
8✔
49
          assessment = assessments |> Enum.find(&(&1.id == submission.assessment_id))
8✔
50
          team = teams |> Enum.find(&(&1.id == submission.team_id))
8✔
51
          team_members = team_members |> Enum.filter(&(&1.team_id == submission.team_id))
8✔
52

53
          team_member_users =
8✔
54
            team_members
55
            |> Enum.map(fn team_member ->
56
              users |> Enum.find(&(&1.id == team_member.student_id))
×
57
            end)
58

59
          render(
8✔
60
            CadetWeb.AdminGradingView,
61
            "gradingsummary.json",
62
            %{
63
              user: user,
64
              assessment: assessment,
65
              submission: submission,
66
              team: team,
67
              team_members: team_member_users,
68
              unsubmitter:
69
                case submission.unsubmitted_by_id do
8✔
70
                  nil -> nil
8✔
71
                  _ -> users |> Enum.find(&(&1.id == submission.unsubmitted_by_id))
×
72
                end
73
            }
74
          )
75
        end
76
    }
77
  end
78

79
  def render("gradingsummary.json", %{
80
        user: user,
81
        assessment: a,
82
        submission: s,
83
        team: team,
84
        team_members: team_members,
85
        unsubmitter: unsubmitter
86
      }) do
87
    s
88
    |> transform_map_for_view(%{
89
      id: :id,
90
      status: :status,
91
      unsubmittedAt: :unsubmitted_at,
92
      xp: :xp,
93
      xpAdjustment: :xp_adjustment,
94
      xpBonus: :xp_bonus,
95
      isGradingPublished: :is_grading_published,
96
      gradedCount:
97
        &case &1.graded_count do
8✔
98
          nil -> 0
×
99
          x -> x
8✔
100
        end
101
    })
102
    |> Map.merge(%{
8✔
103
      assessment:
104
        render_one(a, CadetWeb.AdminGradingView, "gradingsummaryassessment.json", as: :assessment),
105
      student: render_one(user, CadetWeb.AdminGradingView, "gradingsummaryuser.json", as: :cr),
106
      team:
107
        render_one(team, CadetWeb.AdminGradingView, "gradingsummaryteam.json",
108
          as: :team,
109
          assigns: %{team_members: team_members}
110
        ),
111
      unsubmittedBy:
112
        case unsubmitter do
113
          nil -> nil
8✔
114
          cr -> transform_map_for_view(cr, %{id: :id, name: & &1.user.name})
×
115
        end
116
    })
117
  end
118

119
  def render("gradingsummaryassessment.json", %{assessment: a}) do
120
    %{
8✔
121
      id: a.id,
8✔
122
      title: a.title,
8✔
123
      assessmentNumber: a.number,
8✔
124
      isManuallyGraded: a.config.is_manually_graded,
8✔
125
      type: a.config.type,
8✔
126
      maxXp: a.questions |> Enum.map(& &1.max_xp) |> Enum.sum(),
8✔
127
      questionCount: a.questions |> Enum.count()
8✔
128
    }
129
  end
130

131
  def render("gradingsummaryteam.json", %{team: team, assigns: %{team_members: team_members}}) do
132
    %{
×
133
      id: team.id,
×
134
      team_members:
135
        render_many(team_members, CadetWeb.AdminGradingView, "gradingsummaryuser.json", as: :cr)
136
    }
137
  end
138

139
  def render("gradingsummaryuser.json", %{cr: cr}) do
140
    %{
8✔
141
      id: cr.id,
8✔
142
      name: cr.user.name,
8✔
143
      username: cr.user.username,
8✔
144
      groupName:
145
        case cr.group do
8✔
146
          nil -> nil
×
147
          _ -> cr.group.name
8✔
148
        end,
149
      groupLeaderId:
150
        case cr.group do
8✔
151
          nil -> nil
×
152
          _ -> cr.group.leader_id
8✔
153
        end
154
    }
155
  end
156

157
  def render("grading_info.json", %{answer: answer, course: course, assessment: assessment}) do
158
    transform_map_for_view(answer, %{
10✔
159
      id: & &1.id,
10✔
160
      prompts: &build_prompts(&1, course, assessment),
10✔
161
      ai_comments: &extract_ai_comments_per_answer(&1.id, &1.ai_comments),
10✔
162
      student: &extract_student_data(&1.submission.student),
10✔
163
      team: &extract_team_data(&1.submission.team),
10✔
164
      question: &build_grading_question(&1, course, assessment),
10✔
165
      solution: &(&1.question.question["solution"] || ""),
10✔
166
      grade: &build_grade/1
167
    })
168
  end
169

170
  def render("grading_summary.json", %{cols: cols, summary: summary}) do
171
    %{cols: cols, rows: summary}
1✔
172
  end
173

174
  defp extract_ai_comments_per_answer(id, ai_comments) do
175
    matching_comment =
10✔
176
      ai_comments
177
      # Equivalent to fn comment -> comment.question_id == question_id end
NEW
178
      |> Enum.find(&(&1.answer_id == id))
×
179

180
    case matching_comment do
10✔
181
      nil -> nil
10✔
NEW
182
      comment -> %{response: comment.response, insertedAt: comment.inserted_at}
×
183
    end
184
  end
185

UNCOV
186
  defp extract_student_data(nil), do: %{}
×
187

188
  defp extract_student_data(student) do
189
    transform_map_for_view(student, %{
10✔
190
      name: fn st -> st.user.name end,
10✔
191
      id: :id,
192
      username: fn st -> st.user.username end
10✔
193
    })
194
  end
195

196
  defp extract_team_member_data(team_member) do
197
    transform_map_for_view(team_member, %{
×
198
      name: & &1.student.user.name,
×
199
      id: :id,
200
      username: & &1.student.user.username
×
201
    })
202
  end
203

204
  defp extract_team_data(nil), do: %{}
10✔
205

206
  defp extract_team_data(team) do
207
    members = team.team_members
×
208

209
    case members do
×
210
      [] -> nil
×
211
      _ -> Enum.map(members, &extract_team_member_data/1)
×
212
    end
213
  end
214

215
  defp build_grading_question(answer, course, assessment) do
216
    %{question: answer.question |> Map.delete(:llm_prompt)}
10✔
217
    |> build_question_by_question_config(true)
218
    |> Map.put(:answer, answer.answer["code"] || answer.answer["choice_id"])
10✔
219
    |> Map.put(:autogradingStatus, answer.autograding_status)
10✔
220
    |> Map.put(:autogradingResults, answer.autograding_results)
10✔
221
  end
222

223
  defp build_prompts(answer, course, assessment) do
224
    if course.enable_llm_grading do
10✔
NEW
225
      AICodeAnalysisController.create_final_messages(
×
NEW
226
        course.llm_course_level_prompt,
×
NEW
227
        assessment.llm_assessment_prompt,
×
228
        answer
229
      )
230
    else
231
      []
232
    end
233
  end
234

235
  defp build_grade(answer = %{grader: grader}) do
236
    transform_map_for_view(answer, %{
10✔
237
      grader: grader_builder(grader),
238
      gradedAt: graded_at_builder(grader),
239
      xp: :xp,
240
      xpAdjustment: :xp_adjustment,
241
      comments: :comments
242
    })
243
  end
244
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc