🎯 ai_rubric_grader
LLM-agnostic, rubric-based automatic grading for Dart
Feed it questions, rubrics and student answers — get back structured, clamped scores with per-rubric rationale. Bring your own LLM.
It's a small, dependency-light, pure-Dart package — no Flutter, no HTTP client baked in — so you can run it server-side (a Cloud Function, a backend, a CLI), which is where your API key belongs.
Why not just ask the model for marks?
Asking an LLM to "grade this and give me the score" and parsing the free text is
fragile: a stray line or markdown fence silently becomes a 0, and the model can
hand out more points than a rubric allows. ai_rubric_grader fixes that:
| Naive approach | ai_rubric_grader |
|
|---|---|---|
| Output | Free text, hope it parses | Strict JSON, schema-guided |
| Totals | Trust the model's sums | Recomputed from your rubrics |
| Over-awarding | Possible | Clamped to [0, points] |
| Missing item | Crash / wrong total | Treated as 0, never crashes |
| Provider lock-in | Hardcoded | Any LLM via one interface |
| Manual criteria | — | autoGraded: false leaves them for a human |
Install
dependencies:
ai_rubric_grader: ^0.1.1
or dart pub add ai_rubric_grader.
Usage
import 'package:ai_rubric_grader/ai_rubric_grader.dart';
final questions = [
Question(
id: 'q1',
prompt: 'What is the derivative of x²?',
answer: '2x', // plain text, LaTeX, transcribed handwriting…
rubrics: const [
Rubric(id: 'r1', description: 'Correct derivative', requirement: 'equals 2x', points: 5),
Rubric(id: 'r2', description: 'Shows working', requirement: 'steps shown', points: 3,
autoGraded: false), // left for a human
],
),
];
final grader = RubricGrader(myLlmClient);
final result = await grader.grade(questions);
print('${result.awarded}/${result.possible} (${result.percentage}%)');
for (final g in result.questionGrades) {
for (final s in g.rubricScores) {
print('${s.rubricId}: ${s.awarded}/${s.possible} — ${s.rationale}');
}
}
Bring your own LLM
Implement the one-method LlmClient for any provider. Request deterministic
output (temperature: 0) and JSON mode where available.
OpenAI adapter (click to expand)
import 'dart:convert';
import 'package:ai_rubric_grader/ai_rubric_grader.dart';
import 'package:http/http.dart' as http;
class OpenAiClient implements LlmClient {
OpenAiClient(this.apiKey, {this.model = 'gpt-4o-mini'});
final String apiKey;
final String model;
@override
Future<String> complete({required String system, required String user}) async {
final res = await http.post(
Uri.parse('https://api.openai.com/v1/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey',
},
body: jsonEncode({
'model': model,
'temperature': 0,
'response_format': {'type': 'json_object'},
'messages': [
{'role': 'system', 'content': system},
{'role': 'user', 'content': user},
],
}),
);
return (jsonDecode(res.body)['choices'][0]['message']['content']) as String;
}
}
🔒 Security: never embed an LLM API key in a mobile/web client — it ships in the binary and can be extracted. Run the grader behind your backend.
API
| Type | Purpose |
|---|---|
Rubric |
A criterion: description, requirement, points, autoGraded. |
Question |
prompt, answer, rubrics; exposes totalPoints / autoGradablePoints. |
RubricGrader |
grade(List<Question>) → Future<GradingResult>. |
GradingResult |
awarded, possible, percentage, questionGrades. |
RubricScore |
Per-rubric awarded (clamped), possible, rationale, isFullyMet. |
LlmClient |
The one method you implement to plug in a provider. |
All models are immutable and JSON-serializable (toJson / fromJson).
Related packages
Part of the Flutter Grading Toolkit:
rubric_builder— author rubrics in a Flutter UI.handwriting_answer_pad— capture handwritten answers.image_white_background— prep images for vision models.
Contributing
Issues and PRs welcome. Run dart test before submitting.
License
MIT © 2026 Muhammad Ali
Libraries
- ai_rubric_grader
- LLM-agnostic, rubric-based automatic grading for Dart.