Multi-school, multi-tenant AI-native learning platform foundation.
This repository currently contains the first local foundation POC:
- FastAPI modular monolith under
services/api. - Configuration-driven seed data for two isolated tenants.
- Local identity adapter with a Keycloak/OIDC boundary.
- Shared-schema PostgreSQL + pgvector migration with fail-closed RLS policies.
- Tenant-aware deterministic RAG with citation and refusal behavior.
- M3a text ingestion foundation: durable tenant-scoped jobs, deterministic 1536-dimension embeddings, exact pgvector retrieval, and ACL deny enforcement.
- M3b grounding/isolation regression gate with a synthetic corpus, strict citation/refusal/isolation metrics, and a CLI runner for both repository modes.
- M3c STORM-compatible research-job foundation: durable administrator jobs, approved course-source policy, deterministic cited briefs, per-tenant active-job limits, and internal budget units without external provider calls.
- Managed-Moodle integration foundation: tenant-scoped connections, external-ID mappings, idempotent sync jobs, retry state, and reconciliation reports.
- M4b.2 Student Web under
apps/student-web: tenant-bound grounded chat/citations, a course-content view for lessons and assignments, and the existing student submission/update flow. - M4a tenant-health Admin Web under
apps/admin-web: an administrator-only, tenant-scoped, read-only integration and reconciliation dashboard. - M4b.1 Knowledge Operations under
apps/admin-web/knowledge-operations: administrator-only, tenant-scoped review of document lifecycle metadata and safe research-job citation outcomes. - M4c Mobile under
apps/mobile: React Native + Expo tenant-bound student shell with local development profiles, grounded citations/refusals, learning-flow review, and student submission updates. - ADRs, data model, roadmap, and implementation plan under
docs/.
The dev identity adapter is for local testing only. It is not a production authentication mechanism.
cd services/api
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e ".[test]"
uvicorn app.main:app --reload --port 8000Test identities:
-
Bearer dev-saodo-admin→ Tenant A school administrator for integration APIs -
Bearer dev-saodo-student→ Tenant A seed -
Bearer dev-demo-student→ Tenant B seed
cd apps/student-web
npm install
npm run devOpen http://localhost:3000. The development tenant switcher reads public/dev-profiles.json; production tenant discovery must come from domain, Keycloak claims, membership, or LTI registration.
/has working Trợ lý học tập and Nội dung môn học views. It loads only the authenticated profile's tenant config, identity, courses, and selected-course learning flow.- The assistant renders grounded citations or an explicit source-unavailable response. Conversations are scoped to the active local profile and course; changing either clears or hides prior state before fresh data renders.
- An open assignment can be submitted or updated through the existing student-only submission endpoint. The browser never sends
tenant_id; the only Student Web mutations are the existing chat and assignment-submission POST requests. - Run
npm testfor the dependency-free client-state isolation checks, thennpm run lintandnpm run build.
Start the FastAPI service, then run the Expo client:
cd services/api
uvicorn app.main:app --reload --port 8000
cd ../../apps/mobile
npm install
Copy-Item .env.example .env
npm run androidThe checked-in .env.example uses http://10.0.2.2:8000, Android emulator access to the host API. For iOS or a physical device, set EXPO_PUBLIC_API_URL to a reachable LAN API origin and restart Expo; expose Uvicorn with --host 0.0.0.0 only on a trusted development network. The profile selector is development-only, no mobile request sends tenant_id, and release builds intentionally have no embedded dev token. See the mobile README for the full network and verification instructions.
Start the API with both local web origins allowed:
cd services/api
$env:ALLOWED_ORIGINS = "http://localhost:3000,http://localhost:3001"
uvicorn app.main:app --reload --port 8000Then start the standalone Admin Web:
cd apps/admin-web
npm install
npm run devOpen http://localhost:3001. Its local profile selector is development-only: production tenant and identity resolution remain server-owned through the authenticated request context.
/is the M4a Tenant Health dashboard. It fetches onlyGET /api/v1/admin/tenant-health./knowledge-operationsis the M4b.1 review desk. It fetches onlyGET /api/v1/admin/knowledge-operationsand displays document titles/course labels/visibility/lifecycle plus safe research-job/citation metadata.
Neither surface sends a tenant_id, exposes a tenant selector, queues or processes a job, retries work, or returns document content, ACL subjects, worker payloads, idempotency keys, requester details, or research outlines.
docker compose -f infrastructure/docker/docker-compose.yml up -dThe compose file provides PostgreSQL with pgvector, Redis, and Keycloak dev mode. The API uses the deterministic repository by default; set USE_DATABASE_REPOSITORY=true to use PostgreSQL-backed reads.
PostgreSQL is exposed on host port 55432 to avoid conflicts with a local PostgreSQL service; the container still listens on 5432.
To seed and use PostgreSQL-backed reads locally:
cd services/api
pip install -e ".[test]"
python scripts/seed_db.py
$env:USE_DATABASE_REPOSITORY = "true"
uvicorn app.main:app --reload --port 8000For an existing Docker volume, apply the learning-flow, integration, and document-ingestion migrations once before seeding:
docker compose -f infrastructure/docker/docker-compose.yml exec -T postgres psql -U platform -d platform -f /docker-entrypoint-initdb.d/002_learning_flow.sql
docker compose -f infrastructure/docker/docker-compose.yml exec -T postgres psql -U platform -d platform -f /docker-entrypoint-initdb.d/003_integrations.sql
docker compose -f infrastructure/docker/docker-compose.yml exec -T postgres psql -U platform -d platform -f /docker-entrypoint-initdb.d/004_document_ingestion.sql
docker compose -f infrastructure/docker/docker-compose.yml exec -T postgres psql -U platform -d platform -f /docker-entrypoint-initdb.d/005_research_jobs.sqlOnly school_admin and platform_admin contexts can queue or inspect a text-ingestion job. The public API never executes ingestion inline: a worker receives the persisted job metadata and calls the worker adapter under the original tenant/requester context.
$headers = @{ Authorization = "Bearer dev-saodo-admin"; "Idempotency-Key" = "local-document-001" }
$body = '{"title":"Market note","source_uri":"moodle://course-saodo-eco101/resources/market-note","content":"Supply and demand meet at market equilibrium.","visibility":"course","course_id":"course-saodo-eco101"}'
Invoke-RestMethod -Method Post -Uri http://localhost:8000/api/v1/documents/ingestion-jobs -Headers $headers -ContentType "application/json" -Body $bodyThe current provider is deterministic and dependency-free so the local pilot works without an external API key. It is an interface-compatible development/pilot adapter, not a semantic-quality claim. PostgreSQL performs exact cosine search after tenant, course, publication, and ACL filters; a production embedding provider, ANN index, queue runtime, file parsing, and evaluation corpus remain later M3 work.
Only school_admin and platform_admin contexts can queue or inspect a research job. The request selects one to five existing, published documents from the specified course. The worker recreates the persisted requester context and produces a deterministic, cited brief; there is intentionally no public endpoint that executes a job.
$headers = @{ Authorization = "Bearer dev-saodo-admin"; "Idempotency-Key" = "local-research-001" }
$body = '{"topic":"How does market equilibrium change when demand shifts?","course_id":"course-saodo-eco101","source_document_ids":["doc-saodo-eco-market"],"budget_units":20}'
Invoke-RestMethod -Method Post -Uri http://localhost:8000/api/v1/research-jobs -Headers $headers -ContentType "application/json" -Body $bodyThe current foundation does not crawl URLs, call web search, or call an LLM. It uses a per-tenant limit of one queued/running job and checks an internal estimate of 10 units per source against the supplied 10–100 unit budget. A future external STORM adapter needs a school-approved source policy, credentials, spending/rate limits, model/version choice, retention controls, and Architecture Review Board approval. See the research worker contract.
Only school_admin and platform_admin contexts can view integration metadata, queue a sync, inspect mappings, or read reconciliation status. A queued job is idempotent per tenant, integration, and Idempotency-Key; the worker-facing processor is intentionally not exposed as a public HTTP route.
$headers = @{ Authorization = "Bearer dev-saodo-admin"; "Idempotency-Key" = "local-course-sync-001" }
Invoke-RestMethod -Method Post -Uri http://localhost:8000/api/v1/integrations/integration-saodo-moodle/sync-jobs -Headers $headers -ContentType "application/json" -Body '{"operation":"course_catalog"}'The checked-in Moodle connections are deterministic sandbox records. A managed connection uses a secret reference only; never place a Moodle token in seed data, the database, source control, or a public API response. See the Moodle connector contract for onboarding requirements.
cd services/api
pytest -q
python scripts/run_evaluations.py
python -c "from app.main import app; print(app.title)"
cd ../../apps/student-web
npm test
npm run build
cd ../admin-web
npm run lint
npm run build
cd ../mobile
npm test
npm run typecheck
npm run export:androidRead the foundation design, the M4b.2 design, the M4c design, the M3a design, the M3c design, ADR-002, the data model, and the Moodle connector contract. M4 experience surfaces are complete. The next gate is deferred pilot hardening: managed-Moodle onboarding, real Keycloak validation, queue deployment, SIS audit, operational controls, and closed-beta readiness.