🎯 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.

pub package pub points likes CI license: MIT


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).

Part of the Flutter Grading Toolkit:

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.