-
Notifications
You must be signed in to change notification settings - Fork 10
feat(docs): Add overview for AI Assessments #1017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2dd81a8
5fb8cdf
e35ced1
87823fd
22ab98c
425e061
ae701f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # AI Assessments — Getting Started | ||
|
|
||
| **An assessment uses an LLM to grade your items against a rubric and gives you back a structured result** (scores, reasoning, feedback) for every item — not free text, but a fixed JSON shape you choose. | ||
|
|
||
| You give Kaapi two things: | ||
|
|
||
| 1. A **config** — your rubric (the grading instructions), the model to use, and the exact result shape you want back. | ||
| 2. Your **items** — the rows you want graded (text and/or image/PDF URLs). | ||
|
|
||
| Kaapi grades every item and delivers the results to your **webhook**. | ||
|
|
||
| --- | ||
|
|
||
| ## The whole flow in three steps | ||
|
|
||
| | Step | You do | Kaapi does | | ||
| |---|---|---| | ||
| | **1. Create a config** | Save an `ASSESSMENT` config once (`POST /configs`) | Stores it, versioned | | ||
| | **2. Submit items** | `POST /assessments` with your rows + a `callback_url` | Returns an `assessment_id`, starts grading in the background | | ||
| | **3. Get results** | Wait for the webhook | POSTs the finished results to your `callback_url` | | ||
|
|
||
| You never poll or wait on the request — submitting returns immediately, and the results arrive later at your webhook. | ||
|
|
||
|  | ||
|
|
||
| **BATCH is fully batched.** Both stages run as provider **batch jobs** — the | ||
| pre-filters run as a batch, and the assessment runs as a batch. Results are | ||
| delivered to your **webhook** when everything completes (no polling). | ||
|
|
||
| --- | ||
|
|
||
| ## Two methods (Kaapi picks for you) | ||
|
|
||
| You never set a "mode". Kaapi looks at your input and decides: | ||
|
|
||
| | Method | When | Input shape | Status | | ||
| |---|---|---|---| | ||
| | **BATCH** | Many items at once | `data` is a list of rows | ✅ Available | | ||
| | **RESPONSE** | A single item, fast | a single `query` | 🚧 WIP (returns `501` today) | | ||
|
|
||
| This guide covers **BATCH**, the method that is live. | ||
|
|
||
| --- | ||
|
|
||
| ## Supported models | ||
|
|
||
| Pick the provider per config (and per pre-filter): | ||
|
|
||
| | Provider | Value in config | Status | | ||
| |---|---|---| | ||
| | OpenAI | `openai` | ✅ | | ||
| | Google (AI Studio / Gemini) | `google` | ✅ | | ||
| | Anthropic (Claude) | `anthropic` | ✅ | | ||
| | Google Cloud / Vertex | — | 🚧 WIP | | ||
|
|
||
| --- | ||
|
|
||
| ## Where to go next | ||
|
|
||
| 1. **[Configuration and versioning](configuration-and-versioning.md)** — build your rubric, choose the model, define the result shape, and manage versions. | ||
| 2. **[API contract](api-contract.md)** — request/response fields, types, status values, and error codes. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| # API Contract — `POST /assessments` | ||
|
|
||
| Precise request and response shapes for the BATCH assessment API. For a | ||
| walkthrough with context, see the [overview](README.md). | ||
|
|
||
| Everything is delivered by **webhook** — there is no status or result poll | ||
| endpoint. RESPONSE-shaped input returns `501` (WIP). | ||
|
|
||
| **Sample input / output JSON files:** | ||
| https://drive.google.com/drive/folders/1BCaauUuXr9DaZTWI-_-x101SDT4ktwp5?usp=share_link | ||
|
|
||
| --- | ||
|
|
||
| ## Request | ||
|
|
||
| `POST /assessments` | ||
|
|
||
| | Field | Type | Required | Notes | | ||
| |---|---|---|---| | ||
| | `config` | object | ✅ | which saved config version to run | | ||
| | `config.id` | UUID | ✅ | config id (must be tagged `ASSESSMENT`) | | ||
| | `config.version` | int ≥ 1 | ✅ | config version to pin | | ||
| | `input` | object | ✅ | a `data` list ⇒ BATCH; a bare `query` ⇒ RESPONSE (501) | | ||
| | `input.query` | string (non-empty) | ✅ | template; `{column}` placeholders filled per row | | ||
| | `input.data` | array (≥ 1) | ✅ | rows; each row is a flat `{ column: string }` object | | ||
| | `callback_url` | URL (**HTTPS**) | ✅ | webhook the result is POSTed to | | ||
| | `request_metadata` | object | optional | echoed back unchanged in the result | | ||
|
|
||
| Rules: | ||
|
|
||
| - **Strict input** — no extra keys are allowed on `input`; a body carrying both | ||
| `data` and `attachments` is rejected. | ||
| - **Rows match the config's `input_schema`** — every declared column present, no | ||
| undeclared columns, `image`/`pdf` values must be URLs. Otherwise `422`. | ||
| - **`callback_url`** must be HTTPS and public (private/loopback hosts are rejected). | ||
|
|
||
| ```json | ||
| { | ||
| "config": { "id": "a9015dbf-…", "version": 1 }, | ||
| "input": { | ||
| "query": "Grade {answer_sheet} for submission {submission_id}.", | ||
| "data": [ | ||
| { "submission_id": "s1", "answer_sheet": "https://cdn.example.com/s1.jpg" } | ||
| ] | ||
| }, | ||
| "callback_url": "https://your-app.example.com/webhooks/assessment", | ||
| "request_metadata": { "batch": "class7-term1" } | ||
| } | ||
| ``` | ||
|
|
||
| ### Building the batch input | ||
|
|
||
| The `input` object is built from your configuration's `input_schema`: | ||
|
|
||
| 1. **One object per item** goes in `input.data`. Each object's keys are the column | ||
| names declared in the config's `input_schema`, and the values are strings. | ||
| 2. **Attachment columns** (`image` / `pdf`) take a URL string; text columns take | ||
| plain text. | ||
| 3. **`input.query`** is a template. Any `{column}` placeholder is replaced with | ||
| that row's value at grading time, so one template applies to every row. | ||
| 4. **Match the schema exactly** — every declared column present, no extra columns. | ||
|
|
||
| Example: for `input_schema = { submission_id: text, answer_sheet: image(url) }`, | ||
| each row is `{ "submission_id": "...", "answer_sheet": "https://..." }` and the | ||
| `query` can reference `{submission_id}` and `{answer_sheet}`. | ||
|
|
||
| --- | ||
|
|
||
| ## Response — submit acknowledgement (`200`) | ||
|
|
||
| Returned immediately; contains no results. Wrapped in the standard envelope | ||
| `{ success, data, error, metadata }`. | ||
|
|
||
| | Field (`data`) | Type | Notes | | ||
| |---|---|---| | ||
| | `assessment_id` | UUID | correlate with the webhook | | ||
| | `status` | enum | `PROCESSING` on accept | | ||
| | `message` | string | human-readable | | ||
| | `inserted_at` / `updated_at` | timestamp | ISO-8601 | | ||
|
|
||
| ```json | ||
| { | ||
| "success": true, | ||
| "data": { | ||
| "assessment_id": "8a2a7bc1-…", | ||
| "status": "PROCESSING", | ||
| "message": "Your assessment is being processed", | ||
| "inserted_at": "2026-08-12T10:15:30Z", | ||
| "updated_at": "2026-08-12T10:15:30Z" | ||
| }, | ||
| "error": null, | ||
| "metadata": null | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Webhook — the result (POST to `callback_url`) | ||
|
|
||
| Delivered once, on completion. | ||
|
|
||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `assessment_id` | UUID | matches the ack | | ||
| | `status` | enum | terminal (see below) | | ||
| | `data` | object | the `AssessmentBatchResult` (BATCH) | | ||
| | `request_metadata` | object \| null | echoed from the request | | ||
|
|
||
|
Comment on lines
+98
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -i 'api-contract\.md|callbacks\.py|assessment' . | head -80
printf '%s\n' '--- documentation excerpt ---'
sed -n '75,125p' docs/architecture/assessment/api-contract.md
printf '%s\n' '--- callback implementation and usages ---'
ast-grep outline backend/app/services/assessment/api/callbacks.py 2>/dev/null || true
sed -n '1,140p' backend/app/services/assessment/api/callbacks.py
rg -n -C 4 'send_callback|webhook_secret|callback_url|HMAC|signature|sha256|X-.*Sign' backend docs tests 2>/dev/null | head -240Repository: ProjectTech4DevAI/kaapi-backend Length of output: 29561 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- definitions ---'
rg -n -C 12 'def send_callback|def get_webhook_secret|X-Webhook-Signature|X-Webhook-Timestamp|webhook_secret' backend/app backend/app/tests docs/architecture/assessment docs/architecture/kaapi-ai-assessment-ARCHITECTURE.md
printf '%s\n' '--- relevant utility files ---'
fd -i '.*(utils|callback|webhook).*\.py$' backend/app | head -80
printf '%s\n' '--- callback-related tests ---'
rg -n -C 10 'send_callback|webhook|signature|timestamp|callback' backend/app/tests | head -300Repository: ProjectTech4DevAI/kaapi-backend Length of output: 50388 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- utility module locations ---'
rg --files backend/app | rg '(^|/)(utils|__init__)\.py$|utils/'
printf '%s\n' '--- send_callback definition ---'
rg -n -l '^def send_callback|^async def send_callback|send_callback\s*=' backend/app
printf '%s\n' '--- secret resolver definition ---'
rg -n -l '^def get_webhook_secret|^async def get_webhook_secret|get_webhook_secret\s*=' backend/app
printf '%s\n' '--- exact implementation excerpts ---'
for f in $(rg -l '^def send_callback|^async def send_callback|send_callback\s*=' backend/app); do
echo "### $f"
rg -n -C 30 'def send_callback|async def send_callback|send_callback\s*=' "$f"
done
for f in $(rg -l '^def get_webhook_secret|^async def get_webhook_secret|get_webhook_secret\s*=' backend/app); do
echo "### $f"
rg -n -C 20 'def get_webhook_secret|async def get_webhook_secret|get_webhook_secret\s*=' "$f"
done
printf '%s\n' '--- assessment callback tests/files ---'
rg --files backend/app | rg 'assessment.*(test|callback)|callback.*assessment|test.*callback'
rg -n -C 8 'assessment.*callback|callbacks\.deliver|webhook_secret|X-Webhook' backend/app/tests/assessment backend/app/tests 2>/dev/null | rg -m 160 'assessment|callbacks|webhook|signature|timestamp'Repository: ProjectTech4DevAI/kaapi-backend Length of output: 26699 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- signing helper and sender ---'
sed -n '450,625p' backend/app/utils.py
printf '%s\n' '--- callback utility tests ---'
rg -n -C 12 'signature|timestamp|webhook_secret|send_callback|X-Webhook' backend/app/tests/core/test_callback_ssrf.py
printf '%s\n' '--- assessment callback model and final payload path ---'
rg -n -C 12 'class AssessmentCallback|AssessmentCallback|def _finalize|def _fail|callbacks\.deliver|callback_url' backend/app/models/assessment backend/app/services/assessment/api
printf '%s\n' '--- existing documentation of signing contract ---'
rg -n -C 8 'X-Webhook-Signature|X-Webhook-Timestamp|HMAC-SHA256|signing_string|timestamp' docs backend/app | head -220Repository: ProjectTech4DevAI/kaapi-backend Length of output: 50389 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
import hashlib
import hmac
import json
payload = {
"success": True,
"data": {
"assessment_id": "8a2a7bc1-0000-0000-0000-000000000000",
"status": "COMPLETED",
"data": {"total_items": 1, "counts": {}, "items": []},
"request_metadata": None,
},
"error": None,
"metadata": None,
}
raw_body = json.dumps(payload, separators=(",", ":")).encode()
timestamp_ms = 1700000000000
signature = hmac.new(
b"example-secret",
f"{timestamp_ms}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
print("raw_body_utf8_compact =", raw_body.decode())
print("signing_string_prefix =", f"{timestamp_ms}.")
print("signature_hex_length =", len(signature))
print("signature =", signature)
PYRepository: ProjectTech4DevAI/kaapi-backend Length of output: 538 Document webhook signature verification. When 🤖 Prompt for AI Agents |
||
| `data` (`AssessmentBatchResult`): | ||
|
|
||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `total_items` | int | number of input rows | | ||
| | `counts.assessed` | int | rows graded | | ||
| | `counts.filtered` | int | rows gated out by a pre-filter | | ||
| | `counts.errors` | int | rows with an error | | ||
| | `items` | array | one `AssessmentResult` per input row, in order | | ||
|
|
||
| `items[]` (`AssessmentResult`): | ||
|
|
||
| | Field | Type | Notes | | ||
| |---|---|---| | ||
| | `output.assessment` | object \| string \| null | your `json_output_schema` filled in; string for free-text; `null` if gated out / failed | | ||
| | `output.pre_filter.topic_relevance` | `{verdict: bool, reasoning: string}` \| null | null if not configured | | ||
| | `output.pre_filter.duplicate_detection` | `{verdict: bool, reasoning: string}` \| null | null if not configured | | ||
| | `error` | string \| null | per-row error | | ||
|
|
||
| ```json | ||
| { | ||
| "assessment_id": "8a2a7bc1-…", | ||
| "status": "COMPLETED", | ||
| "data": { | ||
| "total_items": 2, | ||
| "counts": { "assessed": 1, "filtered": 1, "errors": 0 }, | ||
| "items": [ | ||
| { | ||
| "output": { | ||
| "assessment": { "score": 20, "feedback": "…" }, | ||
| "pre_filter": { "topic_relevance": { "verdict": true, "reasoning": "…" } } | ||
| }, | ||
| "error": null | ||
| }, | ||
| { | ||
| "output": { | ||
| "assessment": null, | ||
| "pre_filter": { "topic_relevance": { "verdict": false, "reasoning": "off-topic" } } | ||
| }, | ||
| "error": null | ||
| } | ||
| ] | ||
| }, | ||
| "request_metadata": { "batch": "class7-term1" } | ||
| } | ||
|
Comment on lines
+128
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Correct the webhook wire shape and status placement.
Expected outer envelope{
"success": true,
"data": {
"assessment_id": "...",
"status": "COMPLETED",
"data": {},
"request_metadata": null
},
"error": null,
"metadata": null
}Also applies to: 155-165 🤖 Prompt for AI Agents |
||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Status values | ||
|
|
||
| | Status | Meaning | | ||
| |---|---| | ||
| | `PENDING` | accepted, not started | | ||
| | `PROCESSING` | grading in progress (the ack status) | | ||
| | `COMPLETED` | all rows graded, no errors | | ||
| | `COMPLETED_WITH_ERRORS` | finished, some rows errored | | ||
| | `FAILED` | the run failed | | ||
|
|
||
| `status` lives on the envelope only — it is never duplicated inside `data`. | ||
|
|
||
| ## Error codes (at submit) | ||
|
|
||
| | Code | When | | ||
| |---|---| | ||
| | `422` | invalid body, or a row doesn't match `input_schema`, or a non-HTTPS/private `callback_url` | | ||
| | `404` | config id not found | | ||
| | `501` | RESPONSE-shaped input (single `query`) — WIP | | ||
| | `503` | failed to dispatch for processing (retry) | | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Align the output guarantee with the API contract.
This guide says every result has a fixed JSON shape and is not free text.
docs/architecture/assessment/api-contract.mdLine 120 permitsoutput.assessmentto be a string, anddocs/architecture/kaapi-ai-assessment-ARCHITECTURE.mdLines 148-151 makejson_output_schemaoptional. Either makejson_output_schemarequired or state that assessments without it can return free text.🤖 Prompt for AI Agents