Domain · Specifications

Metis — Technical Specifications

Metis runs as three cooperating process types, all built from the same Docker image (services/metis/docker-compose.yml).

13sections33 minread

On this page

Metis is the education and course-generation domain of the Oshun platform. The production implementation is a Python backend service located at services/metis/. This document specifies what is actually implemented in that service — every table, every endpoint, every enumeration, and every configuration variable — grounded in the real source code.

  • Language / runtime: Python 3.11+, FastAPI, SQLAlchemy 2.0 (async), Celery
  • Package name: metis-backend (pyproject.toml), version 0.1.0
  • Application factory: metis.main:create_app (services/metis/src/metis/main.py)
  • API mount prefix: all domain routers mount under /api
  • OpenAPI artifact: services/metis/openapi/metis.openapi.json (OpenAPI 3, 149 path items, 174 operations, 519 component schemas at the time of writing)

Note: a separate, large TypeScript library tree exists at libs/metis/*. This specification documents the deployed Python service under services/metis/, which is the runnable Metis backend.


1. Service Architecture#

1.1 Process Topology#

Metis runs as three cooperating process types, all built from the same Docker image (services/metis/docker-compose.yml). The split between API, worker, and beat ensures that long-running background jobs cannot starve synchronous HTTP request handling.

Process Command Role
metis-api gunicorn metis.main:app with UvicornWorker (×4) FastAPI REST API
metis-worker celery -A metis.tasks.celery_app:celery_app worker Celery background task worker
metis-beat celery -A metis.tasks.celery_app:celery_app beat Celery periodic-task scheduler

The production container (services/metis/Dockerfile) is a two-stage Python 3.11-slim build: uv installs dependencies plus gunicorn; the runtime stage adds curl, tesseract-ocr, and tini, runs as a non-root metis user, and exposes port 8000 with a /health HEALTHCHECK.

1.2 Application Lifecycle#

The FastAPI application is constructed by a factory function rather than at module level, which makes it easy to instantiate with different settings in tests. create_app() configures the FastAPI app with: CORS middleware, an X-Request-Time timing middleware, structured-logging configuration via structlog, optional OpenTelemetry instrumentation, exception handlers, health endpoints, and all API routers. The lifespan context manager initializes the async database engine and the Redis connection pool on startup and closes them on shutdown (metis.deps).

docs_url / redoc_url / openapi_url are served in non-production environments only.

1.3 Infrastructure Dependencies#

The table below lists every external service Metis depends on and the library used to connect to it. All defaults are calibrated for the local docker-compose stack; production deployments override each variable via environment.

Dependency Library / driver Local default (docker-compose)
PostgreSQL SQLAlchemy 2.0 + asyncpg postgresql+asyncpg://oshun:oshun_dev@.../oshun_dev
Redis redis.asyncio redis://localhost:6379/0
Celery celery[redis] broker redis://.../1, result backend redis://.../2
Object store boto3 (S3 / MinIO) http://localhost:9000 (minioadmin)
Email (dev) smtplib → Mailpit development notification delivery

The root docker-compose.dev.yml supplies PostgreSQL, Redis, MinIO, and Mailpit. There is no RunPod, no managed GPU fleet, no AWS ECS/CloudFront infrastructure, and no Metis-specific Terraform in the repository.


2. Persistence — Relational Schema#

SQLAlchemy ORM models live in services/metis/src/metis/models/. The schema is created by the Alembic migration alembic/versions/001_initial.py (revision = "001", create date 2026-03-05), which defines 12 tables. PostgreSQL is used; pgvector is not used by this service.

2.1 TimestampMixin#

Every table inherits from metis.models.base.TimestampMixin, which provides three universal columns. Using a mixin rather than repeating these columns ensures consistent types and default values across the entire schema.

Column Type Notes
id UUID (PK, indexed) default uuid.uuid4
created_at DateTime(timezone=True) set on insert; server default now()
updated_at DateTime(timezone=True) set on insert; onupdate on every UPDATE

2.2 users#

Platform user accounts (metis.models.user.User). The role column controls which API endpoints a user can reach; preferences stores the full learner preferences struct as a JSON-encoded string, keeping the schema simple while still allowing rich, typed preference access in application code.

Column Type Required Notes
email String(255) yes unique, indexed
name String(255) yes display name
hashed_password String(512) yes bcrypt hash (passlib)
role String(50) yes default student; one of student/instructor/admin
avatar_url String(1024) no
bio Text no
is_active Boolean yes default true
is_verified Boolean yes default false
preferences Text no JSON-encoded LearnerPreferences (see §6.2)

Relationships: authored_courses, enrollments, submissions, tutoring_sessions, achievements.

2.3 courses#

Top-level course container (metis.models.course.Course). The slug field provides a human-readable, URL-safe identifier for courses. The status column implements the three-state lifecycle (draftpublishedarchived) that controls which courses appear in public listings.

Column Type Required Notes
title String(500) yes indexed
slug String(500) yes unique, indexed
description Text no
short_description String(500) no
category String(100) yes default general, indexed
difficulty String(50) yes default beginner; beginner/intermediate/advanced/expert
status String(50) yes default draft; draft/published/archived
thumbnail_url String(1024) no
tags Text no comma-separated tag list
estimated_hours Float no
is_featured Boolean yes default false
enrollment_count Integer yes default 0
average_rating Float no
author_id UUID FK→users yes ON DELETE CASCADE, indexed

Relationships: author, modules (ordered by Module.order), assessments, enrollments. Modules/assessments/enrollments cascade-delete with the course.

2.4 modules#

Ordered section within a course (metis.models.course.Module). Modules exist to give instructors a way to group related lessons and publish them incrementally — a module can be unpublished while its lessons are being drafted.

Column Type Required Notes
title String(500) yes
description Text no
order Integer yes default 0
is_published Boolean yes default false
estimated_minutes Integer no
course_id UUID FK→courses yes ON DELETE CASCADE, indexed

Relationship: lessons (ordered by Lesson.order, cascade delete).

2.5 lessons#

Atomic learning unit within a module (metis.models.course.Lesson). The content_type column drives how the content field is interpreted by the client: markdown text for text type, a video URL for video, and JSON for interactive, quiz, and code_exercise types. The is_free_preview flag allows a subset of lessons to be publicly visible before enrollment, which is used for marketing and learner onboarding.

Column Type Required Notes
title String(500) yes
description Text no
content_type String(50) yes default text; text/video/interactive/quiz/code_exercise
content Text no markdown, video URL, or JSON per type
order Integer yes default 0
duration_minutes Integer no
is_published Boolean yes default false
is_free_preview Boolean yes default false; available without enrollment
resource_urls Text no JSON array of supplementary resource URLs
module_id UUID FK→modules yes ON DELETE CASCADE, indexed

The content hierarchy is three levels: Course → Module → Lesson. There are no Section or ContentBlock tables.

2.6 assessments#

Evaluation instrument attached to a course (metis.models.assessment.Assessment). The max_attempts column set to 0 means unlimited attempts, which is the correct default for practice type assessments used for self-study.

Column Type Required Notes
title String(500) yes
description Text no
assessment_type String(50) yes default quiz; quiz/exam/assignment/practice, indexed
duration_minutes Integer no time limit; NULL = untimed
passing_score Float yes default 70.0; percentage 0–100
max_attempts Integer yes default 3; 0 = unlimited
is_published Boolean yes default false
shuffle_questions Boolean yes default false
show_correct_answers Boolean yes default true
available_from DateTime(tz) no
available_until DateTime(tz) no
course_id UUID FK→courses yes ON DELETE CASCADE, indexed

Relationships: questions (ordered by Question.order), submissions.

2.7 questions#

Single question within an assessment (metis.models.assessment.Question). correct_answer is stored as JSON and is NULL for essay and code questions, which are left to instructor manual grading.

Column Type Required Notes
content Text yes question prompt (markdown)
question_type String(50) yes default multiple_choice; multiple_choice/true_false/short_answer/essay/code
options Text no JSON array of options
correct_answer Text no JSON-encoded; NULL for manually-graded types
explanation Text no shown after answering
points Float yes default 1.0
order Integer yes default 0
difficulty String(50) yes default medium; easy/medium/hard
hint Text no
assessment_id UUID FK→assessments yes ON DELETE CASCADE, indexed

2.8 submissions#

A student's attempt at an assessment (metis.models.assessment.Submission). The academic_integrity_verdict_json column stores a full AcademicIntegrityVerdict from the Themis domain as a JSON string, enabling after-the-fact audits without requiring Metis to join against a remote Themis database.

Column Type Required Notes
answers Text yes JSON map of question_id → answer
score Float no percentage 0–100; NULL until graded
points_earned Float no
points_possible Float no
passed Boolean no meets passing threshold
attempt_number Integer yes default 1
started_at DateTime(tz) no
submitted_at DateTime(tz) no
graded_at DateTime(tz) no
grader_notes Text no instructor manual-grading notes
academic_integrity_verdict_json Text no JSON AcademicIntegrityVerdict from Themis (see §7.4)
is_auto_graded Boolean yes default true
assessment_id UUID FK→assessments yes ON DELETE CASCADE, indexed
user_id UUID FK→users yes ON DELETE CASCADE, indexed

2.9 tutoring_sessions#

Tutoring conversation (metis.models.tutoring.TutoringSession). The optional course_id and lesson_id foreign keys allow a tutoring session to be contextualised within the content hierarchy — the AI tutor can reference the specific lesson the learner is working on — but they are nullable so that open-ended tutoring sessions unrelated to any course are also supported.

Column Type Required Notes
topic String(500) yes
status String(50) yes default active; active/completed/abandoned/escalated, indexed
tutor_type String(50) yes default ai; ai/human/hybrid
ai_model String(100) no model used (e.g. gpt-4o, claude-sonnet-4-20250514)
summary Text no AI-generated post-session summary
satisfaction_rating Integer no 1–5
tokens_used Integer yes default 0
estimated_cost Float yes default 0.0; USD
started_at DateTime(tz) no
ended_at DateTime(tz) no
course_id UUID FK→courses no ON DELETE SET NULL, indexed
lesson_id UUID FK→lessons no ON DELETE SET NULL
user_id UUID FK→users yes ON DELETE CASCADE, indexed

Relationship: messages (ordered by created_at).

2.10 tutoring_messages#

One message in a tutoring session (metis.models.tutoring.TutoringMessage). The sender_type distinguishes who produced the message, which is important for replay and moderation: a human_tutor message must be preserved as-is during an escalation handoff, while an ai message may be subject to quality evaluation.

Column Type Required Notes
content Text yes markdown
sender_type String(50) yes student/ai/system/human_tutor
sender_name String(255) no
message_type String(50) yes default text; text/code/image/suggestion/feedback
tokens_used Integer yes default 0
model_used String(100) no
metadata_json Text no JSON metadata
session_id UUID FK→tutoring_sessions yes ON DELETE CASCADE, indexed

2.11 enrollments#

User ↔ course binding (metis.models.progress.Enrollment). The unique constraint uq_enrollment_user_course prevents a learner from enrolling in the same course twice, which would produce ambiguous progress records.

Column Type Required Notes
status String(50) yes default active; active/completed/dropped/paused, indexed
progress_percent Float yes default 0.0; overall completion 0–100
enrolled_at DateTime(tz) yes
completed_at DateTime(tz) no
last_accessed_at DateTime(tz) no
certificate_url String(1024) no S3 URL of completion certificate PDF
rating Integer no 1–5
review Text no
user_id UUID FK→users yes ON DELETE CASCADE, indexed
course_id UUID FK→courses yes ON DELETE CASCADE, indexed

Unique constraint uq_enrollment_user_course on (user_id, course_id).

2.12 progress#

Per-lesson completion within an enrollment (metis.models.progress.Progress). last_position stores an opaque bookmark string — a video timestamp, scroll offset, or similar — that clients interpret. The unique constraint ensures exactly one progress row exists per (enrollment, lesson) pair.

Column Type Required Notes
completed Boolean yes default false
completed_at DateTime(tz) no
time_spent_seconds Integer yes default 0; cumulative
last_position String(255) no bookmark (video timestamp, scroll)
notes Text no learner's private notes
enrollment_id UUID FK→enrollments yes ON DELETE CASCADE, indexed
lesson_id UUID FK→lessons yes ON DELETE CASCADE, indexed

Unique constraint uq_progress_enrollment_lesson on (enrollment_id, lesson_id).

2.13 achievements#

Gamification achievement (metis.models.progress.Achievement). The unique constraint uq_achievement_user_type_name ensures each achievement can only be earned once — a learner cannot accumulate duplicate "course completion" badges for the same course.

Column Type Required Notes
achievement_type String(100) yes course_completion/streak/perfect_score/first_enrollment/milestone, indexed
name String(255) yes
description Text no
icon_url String(1024) no
points Integer yes default 10
earned_at DateTime(tz) yes
metadata_json Text no JSON additional context
user_id UUID FK→users yes ON DELETE CASCADE, indexed

Unique constraint uq_achievement_user_type_name on (user_id, achievement_type, name).

2.14 File-Backed Persistence#

Several feature areas — source ingestion, lecture packages, credentials, and agent-runtime artifacts — produce large JSON documents that are better stored in object storage rather than relational tables. Relational tables are optimised for querying and joining; these documents are written once, read by ID, and rarely queried by field values.

SourceIngestionService writes to a configurable source_ingestion_storage_dir (default /tmp/metis-source-ingestion) and can issue presigned S3 upload URLs against the metis-uploads bucket.


3. API Surface#

All routers are aggregated in metis.api.__init__.api_router and mounted at /api. Two unprefixed infrastructure endpoints exist on the app root and are intentionally outside the /api prefix so that health checks can be invoked without API authentication:

Method Path Purpose
GET /health Liveness; returns service name/version/environment
GET /ready Readiness; checks PostgreSQL + Redis connectivity

The sixteen domain routers and their prefixes are:

Prefix Router module Tag
/api/auth api/auth.py auth
/api/courses api/courses.py courses
/api/assessments api/assessments.py assessments
/api/tutoring api/tutoring.py tutoring
/api/analytics api/analytics.py analytics
/api/admin api/admin.py admin
/api/curriculum api/curriculum.py curriculum
/api/source-ingestion api/source_ingestion.py source-ingestion
/api/credentials api/credentials.py credentials
/api/agent-runtime api/agent_runtime.py agent-runtime
/api/lti api/lti.py lti
/api/caliper api/caliper.py caliper
/api/qti api/qti.py qti
/api/scorm api/scorm.py scorm
/api/xapi api/xapi.py xapi
/api/oneroster api/oneroster.py oneroster

3.1 Authentication — /api/auth#

These five endpoints handle the complete authentication lifecycle. Registration and login both return a token pair; subsequent requests use the access_token as a bearer header. The refresh endpoint lets clients extend their session without re-entering credentials.

Method Path Auth Purpose
POST /register none Register a user; returns a token pair (201)
POST /login none Authenticate; returns a token pair
POST /refresh none Exchange a refresh token for a new pair
GET /me bearer Current user profile with parsed preferences
PATCH /me bearer Update name/bio/avatar/preferences

3.2 Courses — /api/courses#

The courses router is the core of the Metis content hierarchy. Public listing and detail endpoints require no authentication so that unenrolled visitors can browse the catalog; mutation endpoints require the instructor role.

Method Path Auth Purpose
GET `` (root) none List/filter/search/paginate courses
POST `` (root) instructor Create a course (201)
GET /{course_id} none Get a course with nested modules + concept graph
PATCH /{course_id} instructor Update course fields
DELETE /{course_id} instructor Delete a course (204)
GET /{course_id}/concept-graph none Learner-facing course-linked concept graph
POST /{course_id}/lecture-package instructor Generate a prerecorded lecture package (201)
GET /{course_id}/lecture-package bearer Latest lecture package
GET /{course_id}/lecture-package/assets/{asset_path} bearer Fetch a rendered lecture asset file
POST /{course_id}/lecture-package/evaluation instructor Evaluate lecture media quality
GET /{course_id}/lecture-package/evaluation instructor Latest lecture media evaluation
POST /{course_id}/modules instructor Add a module (201)
PATCH /{course_id}/modules/{module_id} instructor Update a module
DELETE /{course_id}/modules/{module_id} instructor Delete a module (204)
POST /{course_id}/modules/{module_id}/lessons instructor Add a lesson (201)
PATCH /{course_id}/modules/{module_id}/lessons/{lesson_id} instructor Update a lesson
DELETE /{course_id}/modules/{module_id}/lessons/{lesson_id} instructor Delete a lesson (204)
POST /{course_id}/enroll bearer Enroll the current user (201)
POST /{course_id}/progress/{lesson_id} bearer Update lesson progress

3.3 Assessments — /api/assessments#

Assessment endpoints mirror the course pattern: public read, instructor-only mutation, and learner-scoped submission. The calibration and fairness endpoints are further restricted to the course author or an admin because psychometric data about individual learners must not leak to other students.

Method Path Auth Purpose
GET `` (root) none List assessments (filter course/type/published)
POST `` (root) instructor Create an assessment (201)
GET /{assessment_id} none Get an assessment with nested questions
PATCH /{assessment_id} instructor Update an assessment
DELETE /{assessment_id} instructor Delete an assessment (204)
POST /{assessment_id}/questions instructor Add a question (201)
PATCH /{assessment_id}/questions/{question_id} instructor Update a question
DELETE /{assessment_id}/questions/{question_id} instructor Delete a question (204)
POST /{assessment_id}/submit bearer Submit answers; auto-grades (201)
GET /{assessment_id}/submissions bearer Current user's submissions
GET /{assessment_id}/calibration author/admin Item-calibration + mastery-threshold report
POST /{assessment_id}/calibration/recompute author/admin Recompute calibration from live data
GET /{assessment_id}/fairness-review author/admin Fairness + DIF review
POST /{assessment_id}/fairness-review/recompute author/admin Recompute fairness/DIF review

3.4 Tutoring — /api/tutoring#

All tutoring endpoints require at least bearer authentication because tutoring sessions are private to the learner. The voice-runtime endpoints (voice-runtime, voice-runtime/events, voice-runtime/resume) interact with the Psyche live- voice runtime and carry additional Psyche-specific payload fields.

Method Path Auth Purpose
GET /sessions bearer List the user's sessions (filter status)
POST /sessions bearer Start a session (201)
GET /sessions/{session_id} bearer Get a session with messages
PATCH /sessions/{session_id} bearer Update status/rating/summary
POST /sessions/{session_id}/messages bearer Send a student message (201)
POST /sessions/{session_id}/generate-response bearer Generate an AI tutor reply (201)
POST /sessions/{session_id}/voice-runtime bearer Provision/refresh a live voice bridge
POST /sessions/{session_id}/voice-runtime/events bearer Record turn-taking/interruption events
POST /sessions/{session_id}/voice-runtime/resume bearer Resume a paused/interrupted voice runtime
POST /sessions/{session_id}/delivery-health bearer Refresh delivery fallback from health signals
POST /sessions/{session_id}/quality-evaluation bearer Evaluate live tutoring embodiment quality
GET /sessions/{session_id}/quality-evaluation bearer Latest tutoring quality evaluation

3.5 Analytics — /api/analytics#

Analytics endpoints operate on different scopes: the learner dashboard and reminders are scoped to the authenticated user; course analytics are scoped to the course author or admin; platform analytics are admin-only. Redis caching is applied to the two most expensive aggregations (dashboard and platform).

Method Path Auth Purpose
GET /dashboard bearer Personalized learner dashboard (Redis-cached)
GET /reminders bearer Learner reminders / continuation notifications
GET /achievements bearer The user's achievements
GET /course/{course_id} author/admin Per-course analytics (enrollment, completion)
GET /platform admin Platform-wide analytics (Redis-cached)

3.6 Admin — /api/admin#

The admin router contains 30 operations, all requiring the admin role. It covers user management, course approval, content moderation, review queues, incident and complaint workspaces, monitoring and alerting, and source-rights decisions. Operations include: /dashboard, /analytics, /metis-v1-launch-gate, /audit-trail, /incidents/workspace, /complaints/workspace, /complaints/{id}/action, /users (list/detail), /users/{id}/status (PATCH), /users/{id}/role (PATCH), /users/{id}/learner-actions (POST), /courses (list/detail), /courses/{id}/approval (POST), /content/flags and /content/flags/{id}/moderate, /moderation/incidents and /moderation/incidents/{id}/decision, /monitoring, /operator-inspections, /alerts and /alerts/{id} (PATCH), /review-queues (overview / list / detail / decision / workflow-action), and /source-rights (workspace / decision).

3.7 Curriculum — /api/curriculum#

Read-mostly curriculum-discovery endpoints backed by in-code catalogs. Because the curriculum taxonomy is versioned and edited deliberately (not generated at runtime), these endpoints serve static or near-static data and do not need database writes: /subject-taxonomy and /subject-taxonomy/{subject_id}, /subject-taxonomy/lookup (POST), /subject-routes (POST), /seed-packs (+/{discipline_id}), /standards-mappings (+/{discipline_id}), /reviewer-pools (+/{discipline_id}), /gold-sets (+ /release-gate-dashboard, +/{discipline_id}), /cross-core-interlocks (+/{interlock_id}), /safety-policies (+/{discipline_id}, +/{discipline_id}/evaluate POST).

3.8 Source Ingestion — /api/source-ingestion#

The source-ingestion router exposes the full lifecycle of a source package — upload, extract, review, generate, maintain. Each stage builds on the previous one; generation endpoints reject requests until the package has been extracted and approved. The full endpoint set: /uploads (POST presigned targets), package CRUD (``POST/GET,/{package_id}GET),/{package_id}/extract(POST/GET),/{package_id}/review(POST),/{package_id}/evaluate(POST),/{package_id}/outline(POST/GET),/{package_id}/generate(POST/GET),/{package_id}/concepts/graph(POST/GET),/concepts/explore(POST),/concepts/graph/validate(POST),/concepts/graph/repair(POST),/{package_id}/refresh(POST/GET),/{package_id}/retrieval-benchmark(POST/GET),/{package_id}/connectors/preview(POST/GET),/study-workspace(POST),/retrieval/algorithm-docs (GET).

3.9 Credentials — /api/credentials#

Credential endpoints issue, verify, and revoke Open Badges 3.0 and CLR documents. The /verify POST endpoint accepts a full credential document body, which enables QR-code scan flows where the verifier has the credential but no persistent URL: /badges/issue (POST, 201), /badges (GET list), /badges/{credential_id} (GET), /badges/{credential_id}/verify (GET), /badges/{credential_id}/revoke (POST), /verify (POST a presented credential), /clr/{learner_id} (GET).

3.10 Agent Runtime — /api/agent-runtime#

All agent-runtime endpoints require the instructor role. The router manages the full lifecycle of durable agent orchestration plans, from registration and scheduling through evaluation and safety screening: /registry (GET), /orchestration-plans (POST/GET by build_id), /events (GET), /graph-indexes (POST/GET), /output-verifications (POST/GET), /rollouts (POST/GET), /evaluation/gold-sets (GET), /evaluation/rubric-scores (POST/GET), /safety-screenings (POST/GET), /research-integrity/adjudications (POST/GET).

3.11 LMS Interoperability#

These six routers implement the standard protocols that allow Metis to interoperate with institutional LMS platforms. The auth pattern differs by protocol — LTI uses signed JWTs, SCORM uses token-protected runtime state, xAPI and Caliper use learner-scoped bearer tokens.

Prefix Endpoints
/api/lti LTI 1.3 / Advantage: /platforms (POST), /launch (POST), /token (POST), /deep-linking/response (POST), /names-and-roles/{context_id} (GET), /assignments/line-items (POST), /assignments/scores (POST)
/api/qti QTI 3 item bank: /item-bank/import (POST), /item-bank/export (POST)
/api/scorm SCORM 1.2/2004 fallback: /packages (POST), /packages/{id}/manifest (GET), /runtime/commit (POST), /runtime/{package_id}/attempts/{learner_id} (GET)
/api/xapi xAPI / cmi5: /statements (POST/GET), /replay (GET)
/api/caliper IMS Caliper: /events (POST/GET), /export (GET)
/api/oneroster OneRoster: /sync (POST, admin) — dry-run or apply a rostering payload

4. Authentication & Authorization#

Authentication is defined in metis.deps. A new engineer should read this section alongside the API tables above: every endpoint annotated bearer uses get_current_user, instructor uses get_current_instructor_user, and admin uses get_current_admin_user.

  • Scheme: HTTP bearer token. JWT decoding uses python-jose; the algorithm and secret come from settings (jwt_algorithm default HS256).
  • Token claims read: sub (user id, required), email, role, name, exp. Missing sub or an expired exp yields HTTP 401.
  • CurrentUser: lightweight object with id, email, role, name and helpers is_admin() (role admin) and is_instructor() (role instructor or admin).
  • Role dependencies: get_current_user (any authenticated), get_current_instructor_user (instructor/admin, else 403), get_current_admin_user (admin only, else 403).
  • Token issuance: AuthService issues an access token (jwt_access_token_expire_minutes, default 30) and refresh token (jwt_refresh_token_expire_days, default 7). TokenResponse returns access_token, refresh_token, token_type (bearer), and expires_in.
  • Resource-level checks: several endpoints additionally enforce ownership (e.g. tutoring sessions are accessible only by the owning user; assessment calibration/fairness require the course author or an admin; credentials/xAPI/Caliper restrict learner scope to the caller unless the caller is an instructor).

5. Background Tasks (Celery)#

Celery configuration is built by Settings.get_celery_config() (metis.config) and applied in metis.tasks.celery_app. All task modules and their queue assignments are declared in task_routes so that the worker can be scaled per-queue independently — for example, running more metis.export workers during a large course-export batch without adding capacity to the metis.ai queue.

5.1 Queues#

task_routes map task modules onto named queues: metis.export, metis.notifications, metis.analytics, metis.cleanup, metis.ai. The default queue is metis.default. task_default_retry_delay is 60s and task_max_retries is 3. The metis-worker container consumes all six queues.

5.2 Periodic Tasks (beat_schedule)#

These three tasks run on a fixed schedule driven by metis-beat and require no HTTP trigger. The hourly analytics aggregation keeps the Redis-cached dashboard data fresh; the daily cleanup tasks prevent orphaned data from accumulating.

Task Interval
metis.tasks.analytics.aggregate_analytics hourly (3600 s)
metis.tasks.cleanup.cleanup_expired_sessions daily (86400 s)
metis.tasks.cleanup.cleanup_orphaned_uploads daily (86400 s)

5.3 Task Modules#

Each task module is routed to its own named queue to allow independent scaling. The descriptions below summarize the work each module performs:

  • tasks/export.pyexport_course exports a full course to S3 in json or scorm format (unknown formats fall back to JSON).
  • tasks/notifications.pysend_notification delivers via email (SMTP → Mailpit in dev), push, or inapp channels; plus send_enrollment_welcome and send_grade_notification.
  • tasks/analytics.pyaggregate_analytics.
  • tasks/cleanup.pycleanup_expired_sessions, cleanup_orphaned_uploads, cleanup_expired_cache.
  • tasks/celery_app.py — Celery app construction and prerun/postrun/failure signal handlers, including a dead-letter routing handler for permanently failed tasks.

6. Key Domain Objects & Enumerations (Pydantic Schemas)#

Pydantic schemas live in services/metis/src/metis/schemas/. There are 511 BaseModel classes across the package — the full set of request bodies, response shapes, and domain value objects. The subsections below document the most important enumerations and structural types. Field-level validation is expressed via Pydantic Field constraints and regex patterns.

6.1 Validation Patterns (request schemas)#

Every request schema enforces domain constraints at the HTTP boundary using Pydantic Field constraints and regex patterns, so invalid values are rejected before any service code runs. The table below lists the most important constraints:

| Schema field | Constraint | | ------------------------------------ | ---------------------------------- | ------------- | ------------ | ----------- | ---------------- | | CourseCreate.slug | regex ^[a-z0-9]+(?:-[a-z0-9]+)*$ | | CourseCreate.difficulty | regex ^(beginner | intermediate | advanced | expert)$ | | CourseUpdate.status | regex ^(draft | published | archived)$ | | LessonCreate.content_type | regex ^(text | video | interactive | quiz | code_exercise)$ | | AssessmentCreate.assessment_type | regex ^(quiz | exam | assignment | practice)$ | | AssessmentCreate.passing_score | ge=0, le=100 | | QuestionCreate.question_type | regex ^(multiple_choice | true_false | short_answer | essay | code)$ | | QuestionCreate.difficulty | regex ^(easy | medium | hard)$ | | RegisterRequest.password | min_length=8, max_length=128 | | RegisterRequest.role | regex ^(student | instructor)$ | | EnrollmentUpdate.status | regex ^(active | completed | dropped | paused)$ | | EnrollmentUpdate.rating | ge=1, le=5 | | TutoringSessionCreate.tutor_type | regex ^(ai | human | hybrid)$ | | TutoringMessageCreate.message_type | regex ^(text | code | image | suggestion | feedback)$ |

6.2 LearnerPreferences (schemas/user.py)#

LearnerPreferences is serialized to JSON and stored in users.preferences. It captures every dimension of how a learner wants to be taught and supervised, from learning pace to oversight context. The model_post_init validator enforces cross-field invariants: for example, the independent oversight context automatically clears all oversight roles and sharing flags so a learner cannot accidentally appear supervised.

The key typed unions within LearnerPreferences are:

  • LearnerStudyGoalfoundational-mastery, exam-prep, career-advancement, project-delivery, curiosity
  • LearnerPacelight, steady, accelerated
  • LearnerContentTypevideo, reading, exercise, quiz, tutoring
  • LearnerTutorStylestep-by-step, socratic, direct, direct-explanation, scaffolded-hints, exam-practice, mentor-coaching
  • LearnerExplanationDepthconcise, balanced, deep
  • LearnerOversightContextindependent, minor-supervised, managed-program
  • LearnerOversightRoleteacher, guardian, institution
  • LearnerReminderDaymondaysunday

Bounded numeric fields: weekly_hours_target (1–40), session_minutes (10–180), study_reminder_hour_local (0–23), assessment_reminder_lead_hours (1–168). Field validators normalize aliases and clamp list lengths.

6.3 Tutoring Delivery & Live Voice (schemas/tutoring.py)#

The tutoring schema layer is the most complex in Metis because it models three different delivery channels (text, voice, avatar), a multi-stage fallback state machine, a full voice turn-taking lifecycle, and teacher representation safety governance — all as typed Pydantic unions.

The key literal-union types are:

  • TutoringIntegrityModeteach, hint, practice, do-not-complete-for-me
  • TutoringDeliveryModetext, live_voice, live_avatar
  • TutoringDeliveryFallbackStatenative, fallback
  • TutoringDeliveryFallbackReason — six reasons covering avatar/voice runtime unavailability and latency/fidelity budget failures
  • TutoringLiveVoiceTransportStatenot_requested, provisioning, ready, active, paused, ended, failed
  • TutoringLiveVoiceTurnStateidle, learner_listening, learner_speaking, processing, tutor_speaking, interrupted, resume_ready
  • TutoringLiveVoiceRuntimeEventType — 10 runtime event types (speech start/stop, response start/stop, interruption, connection loss, pause/resume, partial/final transcript)
  • TutoringTeacherRepresentationReleaseStatusdraft, review, approved, restricted, revoked, retired
  • TutoringTeacherRepresentationConsentStatusnot-required, pending, granted, expired, revoked, blocked

TutoringSessionResponse is a rich response object carrying the session's delivery_fallback (TutoringDeliveryFallbackResponse), live_voice (TutoringLiveVoiceRuntimeResponse), teacher_representation (TutoringTeacherRepresentationSafetyResponse), an append-only consent_log, oversight context/roles, and optional escalation/handoff fields.

6.4 Source Ingestion (schemas/source_ingestion.py)#

Source ingestion schemas model the four ways a package can be created and the four scopes to which it can be attached. The discriminated union on SourceScope uses the level field as the discriminator so that Pydantic can deserialize the correct subtype without ambiguity.

  • Ingestion input kinds: FileIngestionInput, UrlIngestionInput, FeedIngestionInput (feed_format: rss/atom/jsonfeed/unknown), PackageIngestionInput (package_type: scorm/imscc/lti_export/ institutional_archive/zip_bundle).
  • SourceScope is a discriminated union over CourseSourceScope, WorkspaceSourceScope, NotebookSourceScope, LearnerSessionSourceScope (discriminator level).
  • SourcePackageReviewStateResponse.statuspending, approved, changes_requested, rejected; risk_levellow/medium/high/ critical.
  • SourcePackageUsagePolicyResponse.usageapproved, review_only, blocked. Grounded outline/generation endpoints require high_stakes_allowed and review status approved.

6.5 Concept Graph (schemas/source_concept_graph.py)#

The concept graph schema models a directed graph of educational concepts extracted from a source package. Nodes are typed by their epistemic role; edges are typed by the pedagogical relationship between concepts. This structure is used for prerequisite chains, graph-aware evidence retrieval, and learning-path generation.

  • ConceptNodeTypetopic, skill, fact, procedure, principle
  • ConceptEdgeTypeprerequisite, related, part_of, generalizes, specializes, enables, conflicts, complements
  • Validation issue severitywarning, critical; concept-graph repair operations are a bounded set of operator-approved mutations.

6.6 Assessment Calibration & Fairness (schemas/assessment.py)#

These schemas surface the psychometric analysis that Metis computes from live learner submission data. They are returned only to course authors and admins because they contain aggregate learner statistics.

AssessmentCalibrationResponse reports per-question calibration (QuestionCalibrationResponse: difficulty index, IRT-style difficulty, discrimination index, sample sizes) and a MasteryThresholdCalibrationResponse (current vs. recommended threshold, balanced accuracy/precision/recall/F1). AssessmentFairnessReviewResponse reports DIF reviews (ItemDIFReviewResponse, recommended action monitor/recalibrate/retire) and adaptation-parity reviews. These are computed from live learner submission data, not from an external psychometrics service.

6.7 Credentials (schemas/credentials.py)#

Credential schemas implement the Open Badges 3.0 and IMS Global CLR standards as Pydantic models, ensuring that every issued credential is a conformant JSON-LD document that any standards-compliant verifier can check.

  • CredentialTypeopen_badge, clr; CredentialStatusactive, expired, revoked; VerificationStatusvalid, invalid, expired, revoked, not_found.
  • CredentialProof uses a DataIntegrityProof with cryptosuite HMAC-SHA256-2026, a canonical hash, and a proof_value.
  • Issued credentials carry Open Badges 3.0 (open_badge) and optional CLR (clr) JSON-LD documents with IMS Global context URLs.

6.8 Agent Runtime (schemas/agent_runtime.py)#

Agent runtime schemas model the lifecycle metadata of autonomous agents and the execution plans they follow. The kill-switch fallback behaviors (pause, skip_stage, text_only, manual_review) are the safety levers that let operators degrade an agent plan gracefully rather than aborting it entirely.

Agent definitions carry literal-union metadata: AgentLifecycleState (active/experimental/deprecated/disabled), AgentReleaseChannel (stable/canary/shadow), AgentCostClass, AgentTrustZone (public/tenant/restricted/operator), AgentRoleVisibility, and AgentFamily. Orchestration plans produce a DAG (AgentDagNodeResponse / AgentDagEdgeResponse), a replay manifest, approval gates, and kill switches (AgentKillSwitchResponse with fallback behaviors pause/skip_stage/ text_only/manual_review). Safety screenings and research-integrity adjudications return pass/review/blocked-style statuses.

6.9 Lecture Packages (schemas/lecture.py)#

Lecture package schemas capture both the generation request (what the instructor wants produced) and the generated result (per-lesson script, captions, slides, render batches, and export bundles). The subtitle_formats, dubbing_locales, and pronunciation_overrides fields are what enable accessible, multi-language lecture delivery.

LectureGenerationRequest controls prerecorded lecture generation: target_locale, voice_pack_id, presentation_template, caption/transcript toggles, subtitle_formats (vtt/srt), dubbing_locales, pronunciation_overrides, and include_scorm_bundle. CourseLecturePackageResponse returns per-lesson packages with script sections, caption cues, slides, diagrams, notes, narration variants, render batches, and export bundles (with provenance and rights metadata).


7. Cross-Domain Integration#

Metis integrates with four external systems. Each integration has a clear owner of the boundary contract: Metis owns the schema mirroring and consuming-side validation; the upstream domain owns adjudication, rendering, and transport.

7.1 AI Providers#

TutoringService (services/tutoring_service.py) calls LLM providers directly via their official SDKs. Settings.ai_provider selects openai or anthropic; the corresponding SDK (openai.AsyncOpenAI / anthropic.AsyncAnthropic) is used with openai_model (default gpt-4o) or anthropic_model (default claude-sonnet-4-20250514). The ai extras group in pyproject.toml provides openai, anthropic, langchain, and chromadb. If no provider key is configured, tutoring falls back to a structured pedagogical response. There is no provider-abstraction layer and no RunPod/GPU dispatch in this service.

7.2 Psyche (Live Voice Runtime)#

Psyche owns audio transport and voice-activity detection. Metis owns the tutoring session and the pedagogical turn-taking state machine. The live-voice tutoring runtime is provisioned through Psyche-namespaced settings (psyche_voice_runtime_provider default psyche-openai-realtime, psyche_voice_runtime_public_url, token TTLs, latency/fidelity budgets). Tutoring sessions can request live_voice / live_avatar delivery and degrade to text via the TutoringDeliveryFallback mechanism when Psyche is unavailable or fails to meet latency/fidelity budgets.

7.3 Yemaya (Lecture Rendering)#

Yemaya owns GPU-bound media rendering. Metis orchestrates what to render and labels the batches with provider yemaya-batch-render and pipeline metis-lecture-render-pipeline. In the current service the LectureGenerationService and lecture_render_pipeline_helpers produce rendered slide/diagram/notes assets and write asset files to local storage; the course lecture-asset endpoint serves those files back. The labeling convention is the contract point: Yemaya workers watch for batches with this provider/pipeline tag to claim and execute them.

7.4 Themis (Academic Integrity)#

Themis owns detection and adjudication; Metis owns the learner-facing presentation and appeal routing. Assessment submissions can carry an AcademicIntegrityVerdict produced by Themis adjudication, stored as submissions.academic_integrity_verdict_json and surfaced as SubmissionResponse.academic_integrity_verdict. The AcademicIntegrityVerdictResponse schema (schemas/assessment.py) mirrors the canonical Themis contract, including detection signals, classifier outputs, evidence excerpts, a decision with appeal path, policy binding, and governance metadata. A model_validator enforces audit-completeness invariants (e.g. violation verdicts require detection signals and evidence excerpts; severe verdicts require human review; clear verdicts cannot recommend sanctions). Governance fields fix source_of_record = "themis" and consuming_domain = "metis".


8. Configuration#

All settings load from METIS_-prefixed environment variables via pydantic-settings (metis.config.Settings). The sections below list every variable with its default value. In local development all defaults work with the docker-compose.dev.yml stack; in staging/production each variable must be explicitly set.

8.1 Application#

Variable Default
METIS_APP_NAME metis-backend
METIS_APP_VERSION 0.1.0
METIS_ENVIRONMENT development (development/staging/production/testing)
METIS_DEBUG false
METIS_LOG_LEVEL INFO
METIS_HOST / METIS_PORT 0.0.0.0 / 8000
METIS_ALLOWED_ORIGINS localhost:3000/4200/5173

8.2 Database#

A postgresql:// URL is auto-rewritten to postgresql+asyncpg:// for the async engine. The sync_database_url property strips the +asyncpg driver suffix so Alembic (which uses a synchronous connection) receives a compatible URL.

Variable Default
METIS_DATABASE_URL postgresql+asyncpg://oshun:oshun_dev@localhost:5432/oshun_dev
METIS_DATABASE_POOL_SIZE 20
METIS_DATABASE_MAX_OVERFLOW 10
METIS_DATABASE_POOL_TIMEOUT 30
METIS_DATABASE_POOL_RECYCLE 1800

8.3 Redis#

Redis serves three separate purposes in Metis: application cache (database 0), Celery broker (database 1), and Celery result backend (database 2). Keeping them on separate Redis logical databases prevents cache eviction from interfering with task queue state.

Variable Default
METIS_REDIS_URL redis://localhost:6379/0
METIS_REDIS_PREFIX metis:
METIS_REDIS_DEFAULT_TTL 3600
METIS_REDIS_SESSION_TTL 86400
METIS_REDIS_CACHE_TTL 900
METIS_REDIS_RATE_LIMIT_TTL 60
METIS_REDIS_MAX_CONNECTIONS 20

8.4 Object Storage (S3 / MinIO)#

Metis uses four S3 buckets, each serving a distinct purpose: course content, rendered assets, exported packages, and staged uploads. Presigned URLs are used for both upload staging (source ingestion) and download delivery (lecture assets, exports).

Variable Default
METIS_S3_ENDPOINT http://localhost:9000
METIS_S3_ACCESS_KEY / SECRET_KEY minioadmin / minioadmin
METIS_S3_REGION us-east-1
METIS_S3_BUCKET_COURSES metis-courses
METIS_S3_BUCKET_ASSETS metis-assets
METIS_S3_BUCKET_EXPORTS metis-exports
METIS_S3_BUCKET_UPLOADS metis-uploads
METIS_S3_PRESIGNED_URL_EXPIRY 3600
METIS_SOURCE_INGESTION_STORAGE_DIR /tmp/metis-source-ingestion

8.5 Authentication#

The METIS_JWT_SECRET default is intentionally weak and must be replaced in any non-development environment. The access token expiry of 30 minutes is deliberately short; the 7-day refresh token allows clients to maintain sessions without re-authentication.

Variable Default
METIS_JWT_SECRET metis-dev-secret-change-in-production
METIS_JWT_ALGORITHM HS256
METIS_JWT_ACCESS_TOKEN_EXPIRE_MINUTES 30
METIS_JWT_REFRESH_TOKEN_EXPIRE_DAYS 7

8.6 Celery#

Task soft and hard time limits (300s / 600s) prevent runaway tasks from tying up workers indefinitely. worker_prefetch_multiplier = 1 ensures each worker fetches at most one task at a time, which is important for long-running export and AI tasks that should not starve short notification tasks.

Variable Default
METIS_CELERY_BROKER_URL redis://localhost:6379/1
METIS_CELERY_RESULT_BACKEND redis://localhost:6379/2
METIS_CELERY_TASK_SOFT_TIME_LIMIT 300
METIS_CELERY_TASK_TIME_LIMIT 600
METIS_CELERY_WORKER_PREFETCH_MULTIPLIER 1

8.7 AI Providers#

METIS_AI_PROVIDER selects between openai and anthropic. If neither API key is set the service starts successfully but tutoring sessions fall back to a structured pedagogical response rather than a live LLM response.

Variable Default
METIS_OPENAI_API_KEY (empty)
METIS_OPENAI_MODEL gpt-4o
METIS_OPENAI_EMBEDDING_MODEL text-embedding-3-small
METIS_ANTHROPIC_API_KEY (empty)
METIS_ANTHROPIC_MODEL claude-sonnet-4-20250514
METIS_AI_PROVIDER openai
METIS_AI_MAX_TOKENS 4096
METIS_AI_TEMPERATURE 0.7

8.8 Live Voice Runtime#

These variables configure the Psyche live-voice bridge. The latency and fidelity budget values are the thresholds at which the delivery-fallback mechanism triggers: if the voice runtime exceeds METIS_PSYCHE_VOICE_LATENCY_BUDGET_MS or falls below METIS_PSYCHE_VOICE_MIN_FIDELITY_SCORE, tutoring degrades from voice to text automatically and a learner-visible disclosure is emitted.

Variable Default
METIS_PSYCHE_VOICE_RUNTIME_PROVIDER psyche-openai-realtime
METIS_PSYCHE_VOICE_RUNTIME_PUBLIC_URL wss://voice.metis.local/live
METIS_PSYCHE_VOICE_RUNTIME_TOKEN_TTL_SECONDS 900
METIS_PSYCHE_VOICE_LATENCY_BUDGET_MS 900
METIS_PSYCHE_VOICE_MIN_FIDELITY_SCORE 0.72
METIS_PSYCHE_AVATAR_LATENCY_BUDGET_MS 1400
METIS_PSYCHE_AVATAR_MIN_FIDELITY_SCORE 0.8

8.9 Observability & Rate Limiting#

OpenTelemetry is opt-in (METIS_OTEL_ENABLED = false by default) to keep development startup fast. METIS_OTEL_TRACES_SAMPLER_RATIO = 1.0 means all traces are recorded when OTel is enabled — reduce this in high-traffic production environments to control span volume.

Variable Default
METIS_OTEL_ENABLED false
METIS_OTEL_SERVICE_NAME metis-backend
METIS_OTEL_EXPORTER_ENDPOINT http://localhost:4317
METIS_OTEL_TRACES_SAMPLER_RATIO 1.0
METIS_RATE_LIMIT_REQUESTS_PER_MINUTE 60
METIS_RATE_LIMIT_BURST 10

8.10 Notifications#

Push notifications are optional. If METIS_PUSH_NOTIFICATION_ENDPOINT_URL is empty the push channel in send_notification is a no-op; email and in-app channels still work.

Variable Default
METIS_PUSH_NOTIFICATION_ENDPOINT_URL (empty)
METIS_PUSH_NOTIFICATION_API_KEY (empty)
METIS_PUSH_NOTIFICATION_TIMEOUT_SECONDS 5.0
METIS_PUSH_NOTIFICATION_OUTBOX_TTL_SECONDS 30 days

9. Observability#

Metis provides three layers of operational visibility, designed so that a problem can be spotted in Grafana (tracing), diagnosed in log aggregation (structured logs), or checked instantly from a load balancer (health endpoints).

  • Structured logging: structlog with a console renderer in development/testing and a JSON renderer in staging/production. Context vars are merged so request-scoped fields propagate across log lines.
  • Tracing: OpenTelemetry is optional (otel_enabled). When enabled, _setup_opentelemetry configures an OTLP gRPC span exporter, a TraceIdRatioBased sampler, a resource with service name/version/environment, and auto-instruments FastAPI (excluding health/ready). SQLAlchemy instrumentation is available via the installed opentelemetry-instrumentation-sqlalchemy package.
  • Request timing: every response carries an X-Request-Time header.
  • Health/readiness: /health (liveness) and /ready (verifies database and Redis; returns 503 if either is down).
  • Error handling: RequestValidationError returns a structured 422 with per-field detail; unhandled exceptions log a full traceback and return a generic 500.

10. Quality Standards#

These standards define the bar that every change to Metis must meet before it is considered shippable. The OpenAPI contract test is particularly important: it prevents accidental schema drift between the live service and the exported metis.openapi.json artifact that downstream consumers depend on.

  • Type checking: mypy in strict mode with the pydantic.mypy plugin (pyproject.toml); tests/build/dist/alembic are excluded.
  • Linting: ruff with rule sets E,W,F,I,B,C4,UP,ARG,SIM,N, line length 100, target-version = py311.
  • Testing: pytest + pytest-asyncio (asyncio_mode = "auto"); test files under services/metis/tests/. Markers: slow, integration, unit.
  • Coverage: branch coverage on src/metis, fail_under = 80.
  • OpenAPI contract: scripts/export_openapi.py and metis.openapi_contracts render a deterministic, key-sorted OpenAPI document with a canonical root-key order; tests/test_openapi_contract.py guards it against drift.

11. Acceptance Criteria#

A change to Metis is acceptance-ready when all six conditions below are satisfied. These criteria exist because each one guards a different class of regression: type errors, logic bugs, schema drift, coverage gaps, auth holes, and cross-domain contract violations.

  1. mypy --strict and ruff pass for services/metis/src.
  2. pytest passes, including the OpenAPI contract test (tests/test_openapi_contract.py) and the export-script test (tests/test_export_openapi_script.py).
  3. New ORM columns are accompanied by an Alembic migration and matching Pydantic request/response schemas.
  4. Branch coverage stays at or above 80%.
  5. New endpoints declare explicit auth dependencies (get_current_user, get_current_instructor_user, or get_current_admin_user) and enforce resource-level ownership where applicable.
  6. Cross-domain payloads (Themis verdicts, Psyche runtime events, Yemaya render results) conform to the schemas in services/metis/src/metis/schemas.

12. Correctness Verification Subsystem#

The correctness-verification subsystem lives in the TypeScript library @metis/verification (with claim/media agents in @metis/agents). It is consumed by content generation and gates release of generated lessons. Unlike the Python backend specced above, it is library code; the contract below describes its types and decision algebra.

12.1 Gate decision algebra#

  • Verifier.verify(content, context) → VerifierResult returns a normalized score (0–1), a calibrated confidence, a passed flag, an optional critique, and may return { notConfigured: true } when its backing model is absent.
  • VerificationGate composes verifiers (marked required or advisory) and aggregates to a GateDecision of pass | needs-human | block:
    • any required verifier that is notConfigured or fails → block (fail-loud; the gate never silently passes a missing required check);
    • low aggregate confidence or high judge disagreement → needs-human;
    • otherwise → pass.
  • composeP0Gate(config) builds the default panel; runVerifiedGeneration drives generate → verify → (regenerate|refine) → re-gate up to a bound; runVerifierGuidedGeneration scores N candidates and selects pessimistically.

12.2 Verifiers#

Verifier Method
Factuality atomic claim decomposition → evidence retrieval → entailment → aggregate; claim→source span linking
Faithfulness (TRACe) groundedness / relevance / completeness over RAG context
Citation sufficiency per-statement support adequacy
Math correctness CAS-lite expression evaluation + identity testing
Code correctness sandboxed JavaScript execution against declared cases/checks
Process (worked steps) per-transition validity of an equation chain
Pedagogical judge panel rubric scoring + ICC consistency / Spearman alignment / discrimination
Selective evaluation conformal threshold (Clopper–Pearson exact, Hoeffding, RCPS) → abstain
Contradiction negation / value-mismatch heuristic + optional NLI seam
Uncertainty calibration expected calibration error + histogram calibrator
Pedagogy (curriculum) Flesch–Kincaid reading level, prerequisite order, misconception scan

12.3 Supporting machinery#

  • Evidence ledgerbuildEvidenceRecord stamps run id, model, content sha256, per-claim verdicts, judge scores, disagreement, and the gate decision; verifyEvidenceBinding detects tampering.
  • Eval harnessscoreVerifier computes precision/recall/F1, Cohen's κ, and calibration error against a versioned JSONL gold set; recorded baselines live in EVAL_BASELINES.md (e.g. the lexical-default factuality baseline: P 0.516 / R 1.0 / F1 0.681 / κ 0.0625 / MAE 0.464, motivating the NLI swap).
  • HITLselectForReview routes low-confidence / high-disagreement items to a versioned FeedbackStore; labels feed judge calibration, the selective threshold, and per-criterion reliability.
  • Budgets & hardeningComputeBudgetMeter + MODE_BUDGETS per call; checkBudgetCeilings asserts p95 latency + total cost against MODE_CEILINGS and recommends a kill-switch; monitorAgreementDrift (Welch z) and evaluateVerifierPromotion (two-proportion z) gate champion-challenger promotion.

13. Agentic Media & Manim Render Service#

The agentic teaching-media pipeline (@metis/agents Planner/Coder/Critic + @metis/multimedia loop/compositor) produces verified animations; the rendering itself runs in a sandboxed Python worker under services/metis/src/metis/media/.

13.1 Author→critic loop#

runAgenticMediaLoop(lesson, seams, config) executes: verify lesson (a block aborts before any media) → plan scenes → for each scene { code → render → critique → (repair) }* until the critic score ≥ minSceneScore or maxIterationsPerScene / maxTotalRenders is hit → optional learning-outcome probe → release decision. The renderer, planner, coder, critic, lesson verifier, and outcome probe are all injected seams.

13.2 Manim render service contract#

ManimRenderService.render(job) executes generated Manim code out-of-process:

  • Request (RenderJob / wire ManimRenderRequest): sceneName, code, resolution {width,height}, fps, format (mp4|webm|mov|gif), outputFrames, timeoutMs.
  • Sandbox: a per-job working directory; the manim CLI is spawned in its own session with a wall-clock timeout (whole process-group killed on expiry) and POSIX RLIMIT_CPU / optional RLIMIT_AS limits; the timeout is clamped to a service ceiling.
  • Response (RenderResult / wire ManimRenderResponse): on success a video path, ffprobe-measured durationSeconds, and optional ffmpeg-extracted framePaths; on failure a structured error of kind syntax | runtime | timeout | unknown with stderr + offending line — never a thrown crash. With no Manim binary the service raises RendererNotConfiguredError (fail-loud, never a fabricated artifact).
  • Transports: a standalone FastAPI app (manim_render_app, POST /render, GET /healthz) and a one-shot stdin/stdout CLI (manim_render_cli); the TS ManimClient reaches either through createHttpManimTransport or createChildProcessManimTransport. The worker requires the render extra (manim) plus the ffmpeg/ffprobe system binaries (and a LaTeX toolchain for MathTex).

13.3 Segment composition#

composeMediaSegment(plan, rendered, options) assembles a finished segment: one full-frame VideoCompositor background layer per rendered scene, laid back-to-back; one real TTS narration AudioSegment per scene, retimed to its window; each scene window widened to fit the longest of {planned, rendered-video, narration} duration so video and speech stay synced; a WebVTT/SRT caption track and a timestamped transcript; and an optional avatar/lip-sync picture-in-picture overlay. A planned scene with no rendered artifact raises MissingSceneRenderError rather than compositing a gap.