A memory-augmented, self-improving learning agent for LMS integration.
Docere started as a coding education platform built in response to Minnesota's low computer science participation rate. Fewer than half of high schools offered CS courses. What began as a tutoring tool for coding quickly grew into a full intelligent learning agent that integrates directly with LMS platforms like Canvas and Moodle via LTI 1.3.
Unlike raw LLMs like ChatGPT or Claude, Docere doesn't just answer questions. It remembers each student's learning history, adaptively picks teaching strategies using bandit algorithms, and scores its own interactions to improve over time. Course materials, grades, and rosters are auto-pulled from the LMS so teachers set up nothing. Because Docere lives inside the LMS rather than beside it, it sees what students are actually working on: their assignments, grades, deadlines, and course materials. Every response is grounded in real classroom context, not generic knowledge. The more it teaches, the better it gets, and so do the students.
Authors: Youdahe Asfaw, Kofi Osei, Guarionex Salivia Institution: Gustavus Adolphus College Paper: "Learning Agents That Learn"
- Architecture Overview
- Core Systems
- Data Model
- API Reference
- Project Structure
- Getting Started
- Configuration Reference
- Research Design
- Roadmap
┌──────────────────────────────────────────────────────────────────┐
│ FastAPI Server │
│ /api/v1/chat /api/v1/lms /api/v1/analytics /api/v1/... │
└───────────────────────┬──────────────────────────────────────────┘
│
┌─────────────▼─────────────┐
│ TutoringAgent │
│ (core/agent.py) │
│ │
│ 1. Retrieve memory ctx │
│ 2. Select strategy │
│ 3. Build system prompt │
│ 4. Call LLM │
│ 5. Post-process │
└──┬────────┬──────────┬────┘
│ │ │
┌────────▼──┐ ┌───▼────┐ ┌──▼──────────────┐
│ Memory │ │Strategy│ │ Process │
│ Layer │ │Archive │ │ Verifier │
│ │ │ (UCB1) │ │ (LLM-as-judge) │
└──┬──┬──┬──┘ └───┬────┘ └──┬──────────────┘
│ │ │ │ │
│ │ │ ┌────▼────┐ │
│ │ │ │Strategy │ │
│ │ │ │Evolver │ │
│ │ │ │(weekly) │ │
│ │ │ └─────────┘ │
│ │ │ │
┌────▼┐ │ ┌▼────────┐ ┌─────▼──────┐
│Qdrant│ │ │PostgreSQL│ │Outcome │
│(vec) │ │ │(relat.) │ │Tracker │
└──────┘ │ └──────────┘ └────────────┘
│
┌──────▼──────┐
│ LMS Adapter │
│ Canvas/ │
│ Moodle │
└─────────────┘
Data flow for a single student message:
- Student sends a message via the chat API
- Memory Layer assembles context by parallel-fetching from three sources: teacher materials (Qdrant), student profile (PostgreSQL), and interaction history (Qdrant)
- Strategy Archive selects a teaching strategy using UCB1 bandit (exploration vs. exploitation), gated by study group
- System prompt is built from memory context + strategy instructions
- LLM generates a tutoring response
- Message pair is persisted and async post-processing fires: memory capture, embedding generation, and process verification scoring
src/docere/core/agent.py
The central orchestrator. For each student message, it coordinates memory retrieval, strategy selection, prompt construction, and LLM invocation.
Student message → Memory retrieval → Strategy selection → Prompt building → LLM call → Persist → Post-process
Key behaviors:
- Feature-gated by
study_group— the control group receives no memory or strategy augmentation, enabling rigorous ablation studies - Conversation history is fetched from PostgreSQL (last 20 messages) and prepended to the LLM call
- Every response records metadata: strategy used, memory tokens consumed, model version
- Post-processing runs asynchronously: memory capture generates an embedding for the Q&A pair and stores it in both Qdrant (for future semantic retrieval) and PostgreSQL (for relational queries)
System prompt composition:
Base tutoring guidelines
+ Strategy instructions (if treatment_full group)
+ Student profile summary
+ Concept mastery levels
+ Recent grades
+ Relevant course materials (from Qdrant)
+ Relevant past interactions (from Qdrant)
src/docere/core/memory/
A three-source semantic memory system with token budgeting.
Parallel-fetches from all three sources using asyncio.gather():
| Source | Storage | Retrieval Method | Purpose |
|---|---|---|---|
| Teacher context | Qdrant | Cosine similarity against student query | Relevant syllabus, lectures, assignments |
| Student profile | PostgreSQL | Direct lookup | Learning patterns, engagement, grades |
| Interaction history | Qdrant | Cosine similarity + diversity filter | Relevant past conversations |
Token budget allocation (default 4,000 tokens max):
- Student profile (highest priority, ~200 tokens)
- Concept mastery levels (~100 tokens)
- Recent grades (~150 tokens)
- Course materials (variable, truncated to fit)
- Interaction history (fills remaining budget)
Maintains per-student, per-course aggregate profiles:
- Engagement level: Classified as
unknown→low→medium→highbased on interaction count thresholds (3, 10, 20) - Confusion tracking: Running average of confusion scores across all interactions
- Concept mastery: Per-concept skill levels (0.0–1.0) adjusted incrementally — low confusion raises mastery by +0.05, high confusion lowers it by -0.03
- Narrative summaries: Auto-generated via LLM, producing human-readable profiles like "Alex has been struggling with recursion for 2 weeks. She responds well to visual analogies and step-by-step examples."
Stores and retrieves tutoring exchanges in Qdrant with a diversity filter:
- Fetches 3x the requested number of results
- Iteratively selects results using a greedy diversity algorithm: each candidate must have cosine similarity below the
diversity_threshold(default 0.65) against all already-selected results - Prevents the context window from being filled with near-duplicate memories
Auto-ingests course materials from the LMS on first LTI launch:
- Chunking: Splits materials at paragraph boundaries (max 2,000 chars per chunk)
- Compression: Materials over 3,000 characters are compressed via CARTRIDGES-inspired synthetic Q&A distillation — the LLM generates 5–10 Q&A pairs capturing key concepts, requirements, and common confusion points
- Retrieval: Cosine similarity search against the student's current query
Periodic compression of raw memories to prevent unbounded growth:
- Triggered when a student exceeds a configurable threshold (default: 50 uncompressed memories)
- Groups memories by type, processes in batches of 20
- LLM generates 3–5 sentence summaries preserving: key struggles, learning patterns, breakthroughs, trajectory
- Originals are marked
is_compressed = Trueand linked to the summary viacompressed_intoUUID - Baseline seed strategies are never compressed
src/docere/core/verification/
Adapts MATH-SHEPHERD's process reward model approach to open-ended educational dialogues. Instead of verifying math steps, Docere scores whether tutoring interactions actually helped students learn.
| Dimension | Range | Weight | Description |
|---|---|---|---|
| Helpfulness | 0.0–1.0 | 0.35 | Did the response address the student's actual need? |
| Clarity | 0.0–1.0 | 0.25 | Was the explanation appropriate for this student's level? |
| Understanding delta | -1.0–1.0 | 0.25 | How much did understanding likely change? |
| Engagement | 0.0–1.0 | 0.15 | Did the response encourage continued productive learning? |
Composite score = 0.35 * helpfulness + 0.25 * clarity + 0.25 * max(0, understanding_delta) + 0.15 * engagement
Two-phase scoring approach:
Phase 1 — Heuristic scoring (immediate, no LLM call):
- Clarity: Sentence length analysis (ideal 10–20 words for tutoring), presence of examples/code blocks, response-to-question length ratio
- Engagement: Time-to-followup mapping — <30s = 0.9 (very engaged), 30s–2min = 0.7, 2–10min = 0.5, 10min–1hr = 0.3, >1hr = 0.1
- Quality: Presence of questions (Socratic engagement bonus), structured formatting (bullet points, numbered lists), length penalties for extremes
Phase 2 — LLM-as-judge (deferred until student responds or 300s timeout):
- Claude evaluates the interaction with the student's followup as evidence
- Student followup is classified into:
understood,still_confused,new_question,off_topic - Final scores are a weighted blend: 70% LLM + 30% heuristic for clarity, 60/40 for engagement
Links interaction scores to downstream LMS grade outcomes:
- When a grade arrives from the LMS sync, searches for tutoring interactions from the past 14 days about related concepts
- Updates the
subsequent_performancefield on matching interaction scores - Enables analysis of whether specific tutoring interactions correlated with better/worse outcomes
src/docere/core/improvement/
The key differentiator: Docere doesn't just remember — it learns which teaching approaches work and evolves new ones.
Maintains a library of teaching strategies, initialized with 5 seed strategies:
| Strategy | Type | Core Approach |
|---|---|---|
| Socratic Questioning | socratic |
Guide through discovery via questions, never give answers directly |
| Analogy-First | analogy |
Real-world analogies before formal concepts |
| Scaffolded Hints | scaffolded |
3-level progressive hints: concept → example → walkthrough |
| Error-Focused | error_focused |
Explain WHY errors happen, address common misconceptions |
| Minimal Intervention | minimal |
Shortest possible hint, encourage productive struggle |
UCB1 multi-armed bandit selection:
UCB1(strategy) = avg_score + sqrt(2 * ln(total_uses) / strategy_uses)
- Balances exploitation (pick what works) vs. exploration (try underused strategies)
- Strategies with 0 uses get
+infscore (forced exploration) - Only active for the
treatment_fullstudy group — control andtreatment_memorygroups receive no strategy augmentation
Outcome recording:
- Running average score maintained per strategy
- Success rate tracked (interactions with composite score >= 0.6)
- Used by the evolver to identify top/bottom performers
Runs on a weekly cycle. Inspired by the Darwin Godel Machine's archive-based exploration:
Step 1 — Mutate top K performers:
- Selects top K strategies by average score (default K=3)
- For each, retrieves best/worst performing interaction contexts
- Claude generates a variant: same core approach but refined based on what worked and what didn't
- New strategy is linked to its parent via
parent_strategy_idwithgenerationincremented - Requires minimum 5 uses before a strategy is eligible for mutation
Step 2 — Prune weak strategies:
- Strategies with >20 uses and avg_score < 0.3 are deactivated (
is_active = False) - Seed (baseline) strategies are never pruned — they serve as the evolutionary foundation
Evolution lineage is fully tracked: parent → child → grandchild, with generation numbers, enabling analysis of whether evolved strategies actually outperform their ancestors.
Manages the ablation study's feature gating:
Deterministic group assignment:
- Uses SHA-256 hash of
(randomization_seed + student_id)for reproducible but random-looking assignment - Supports arbitrary study arms (default:
control,treatment_memory,treatment_full)
Feature flags by group:
| Group | Memory | Verification | Self-Improvement |
|---|---|---|---|
control |
Off | Off | Off |
treatment_memory |
On | Off | Off |
treatment_full |
On | On | On |
All assignment events are logged to research_events for audit trail.
src/docere/integrations/lms/
Zero-configuration LMS integration. Teachers do nothing — everything is auto-pulled.
Defines normalized data structures and abstract methods:
LMSCourse,LMSAssignment,LMSSubmission,LMSEnrollment,LMSCourseMaterialfull_sync(course_id)— pulls everything in one call (first LTI launch)- Individual methods:
get_course(),get_assignments(),get_submissions(),get_enrollments(),get_course_materials()
Implementations: CanvasAdapter, MoodleAdapter
Full sync (on first LTI launch):
- Upsert course (create or update from LMS data)
- Sync student roster → create/update
Userrecords - Sync assignments with descriptions, due dates, points
- Sync submissions/grades
- Sync course materials (files, modules, pages)
Incremental sync (every 2 hours via background task):
- Pulls latest assignments and submissions only
- Updates existing records or creates new ones
- Builds lookup maps for efficient batch processing
Iterates all LMS-enabled courses, instantiates the appropriate adapter, runs incremental sync. Scheduled via ARQ (Redis-backed async task queue).
src/docere/api/gradebook_sync.py, src/docere/api/integrations.py, src/docere/services/google_sheets.py, frontend/src/components/GradebookSyncModal.tsx
Instructors can push grades from an Excel spreadsheet or Google Sheet into their LMS gradebook through a 4-step wizard.
┌─────────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 1. Source │ ──▶ │ 2. Dest. │ ──▶ │ 3. Validate │ ──▶ │ 4. Sync │
│ Selection │ │ Selection │ │ & Fix │ │ to LMS │
└─────────────┘ └─────────────┘ └──────────────┘ └─────────────┘
Two input methods:
Excel Upload (POST /api/v1/integrations/upload-excel):
- Accepts
.xlsxfiles up to 10 MB - Parses with
openpyxlin read-only/data-only mode - Auto-detects the header row: scans from the top and picks the first row with 2+ non-empty cells (handles title rows, merged cells, blank lines at the top)
- Strips trailing empty rows
- Returns
{upload_id, filename, headers, rows, sheet_names} - Parsed data is held in an in-memory cache keyed by
upload_id
Google Sheets (3 sub-routes under /api/v1/gradebook):
| Endpoint | What it does |
|---|---|
GET /sources/google |
Lists sheets the instructor created via Docere (Drive API, drive.file scope) |
POST /sources/google/read |
Reads a sheet by spreadsheet ID (Sheets API v4) |
POST /sources/google/read-url |
Extracts the spreadsheet ID from a pasted URL via regex, then delegates to read |
All Google API calls run through GoogleSheetsService, which:
- Fetches OAuth credentials from
InstructorCalendarTokenin the database - Builds the Sheets/Drive service with
static_discovery=False(required because the Sheets and Drive discovery documents aren't in the static cache) - Wraps every blocking
googleapiclientcall inrun_in_executorto avoid blocking the async event loop
The frontend provides a tab switcher: "Google Sheets" shows the instructor's sheet list plus a paste-URL input; "Excel Upload" shows a drag-and-drop zone. If Google isn't connected or is missing Sheets scopes, the UI shows a "Connect Google" or "Upgrade Permissions" button.
The instructor picks:
- Course — dropdown if they have multiple courses
- Grade items — checkboxes for which LMS assignments to sync
Grade items are fetched from the LMS via GET /api/v1/gradebook/destinations/{course_id}/grade-items, which calls the appropriate adapter:
- Moodle:
mod_assign_get_assignments→ returns[{id, name, category: "assignments", grade_max}] - Canvas:
GET /courses/{id}/assignment_groups?include[]=assignments→ groups assignments by assignment group name
Items are grouped by category in the UI with select-all / deselect-all controls.
This is the core of the system. Validation does three things: column mapping, student matching, and grade parsing.
Column Mapping — 4-Tier Fallback:
The system needs to figure out which spreadsheet column is the student name and which columns correspond to which LMS assignments.
Tier 1 — Heuristic student column detection (_find_student_column):
- Scans headers for keywords: "student", "name", "full name", "learner"
- Fallback: finds the column where most values contain a space and are mostly alphabetic (i.e., look like human names)
Tier 2 — Deterministic header-to-assignment matching (_match_headers_to_items):
- Normalizes both headers and assignment names: lowercase, strip whitespace, remove numbering prefixes like
"1. "or"1) " - Scoring: exact match = 100, substring containment = 80, word overlap >= 50% = 60
- Only accepts matches with score >= 30
- Prevents duplicate column assignments (a column can only match one grade item)
Tier 3 — Claude LLM fallback:
- If deterministic matching finds zero grade column matches, the system sends the headers, 5 sample rows, and the LMS grade items list to Claude with a structured prompt
- Claude returns JSON:
{"student_name_col": <int>, "grade_columns": {"<grade_item_id>": <col_index>}} - The response is parsed with fallback logic to handle Claude returning assignment names instead of IDs (resolved via partial name matching)
Tier 4 — Numeric column auto-assignment:
- If all else fails, the system identifies columns where >= 50% of sample values are numeric
- If there's exactly 1 unmatched grade item and 1 numeric column, they're matched
- If the count of unmatched items equals the count of numeric columns, they're matched in order
If no columns can be matched after all 4 tiers, the endpoint returns HTTP 422 with a helpful error listing expected assignment names.
Student Matching:
For each row:
- Extract the student name from the mapped column
- Look up in
enrollment_map(built fromadapter.get_enrollments(), keyed byname.lower()) - If no exact match, try substring matching in both directions
- If still no match, flag as
missing_studentissue with suggestion
Grade Parsing:
For each grade cell:
- Strip
%suffix ("85%"→"85") - Handle fractions (
"85/100"→"85") - Parse as float
- Range check: must be
0 ≤ grade ≤ grade_max - Non-numeric values (e.g.
"B+","Absent") are flagged asformat_error
Validation Output:
{
"valid": false,
"mappings": {"student_name_col": 0, "grade_columns": {"123": 1, "456": 2}},
"issues": [
{"row": 3, "col": 1, "type": "invalid_grade", "current": "105", "expected": "0-100", "suggestion": "Grade exceeds maximum (100)"},
{"row": 5, "col": 0, "type": "missing_student", "current": "Jon Smith", "expected": "Enrolled student name", "suggestion": "'Jon Smith' not found in course roster"}
],
"preview": [{"student": "Jane Doe", "student_lms_id": "42", "grades": [{"item": "HW 1", "item_id": "123", "new": 85.0}]}],
"student_count": 25
}If valid is true, the frontend shows a preview table and a "Sync to Gradebook" button. If false, it shows an editable spreadsheet with issue cells highlighted in red.
When validation finds issues, the instructor has two options:
"Let Docere Fix It" (POST /api/v1/gradebook/fix):
- Sends the spreadsheet data and issue list to Claude with a rule-based system prompt
- Fix rules applied by Claude:
- Fractions/percentages:
"85/100"→"85","85%"→"85" - Letter grades:
"B+"→"87","A-"→"92","C"→"75"(standard scale) - Attendance:
"Absent"→"0","Excused"/"N/A"→""(empty = skip) - Out-of-range: clamped to valid range (e.g.
105→100) - Missing students: suggest closest matching name from the roster
- Fractions/percentages:
- Returns the corrected
SpreadsheetDataand a changelog of every cell changed with reasons - The frontend auto-revalidates the fixed data immediately
"Fix Manually & Retry":
- The spreadsheet is displayed inline with editable
<input>cells - Issue cells are highlighted red with a tooltip showing the error type and suggestion
- Instructor edits inline, then clicks back to re-validate
This same two-option pattern appears again if any grades fail during the sync step.
Once validation passes, syncing pushes each grade to the LMS one at a time:
- Moodle: calls
mod_assign_save_gradeper student per assignment withworkflowstate="graded" - Canvas:
PUT /api/v1/courses/{course_id}/assignments/{assignment_id}/submissions/{student_id}withposted_grade
Each grade save is individually try/caught — partial success is possible. The result shows {synced: N, failed: M, errors: [...]}:
- All succeeded: green checkmark, "X grades synced"
- Some failed: amber warning with progress bar, error details per student, and the same "Let Docere Fix It" / "Fix Manually & Retry" buttons
Rows can be skipped via the skip_rows parameter in the sync request.
Grades don't just sit in the LMS. They're integrated into Docere's memory layer for personalized tutoring:
-
LMSSyncServiceruns on LTI launch and periodically (every 2 hours). It pulls submissions from the LMS and detects grade changes (comparesnew_score ≠ old_score). -
MemoryLayer.integrate_grade()is called for each change:- Creates a
MemoryRecordof type"grade"with content like"Scored 85/100 (85%) on 'Homework 3'" - Computes confusion:
confusion = 1.0 - (percentage / 100)— a low grade implies high confusion on those topics - Updates concept mastery via Bayesian Knowledge Tracing if concepts are associated with the assignment
- Updates
student_profile.current_gradefor the overall profile
- Creates a
-
Agent context assembly: When the agent builds context for a conversation,
_get_recent_grades()includes the 5 most recent graded submissions. This lets the agent say things like "I see you got 72% on the last quiz — want to review those topics?" -
Instructor dashboard: Grade data also feeds into the instructor-facing at-risk student detection and student cards.
| File | Purpose |
|---|---|
src/docere/api/gradebook_sync.py |
Validate, sync, and fix endpoints |
src/docere/api/integrations.py |
Excel upload/download, action execution |
src/docere/services/google_sheets.py |
Google Sheets read/write/list via Sheets + Drive APIs |
src/docere/integrations/lms/moodle.py |
Moodle adapter: grade items, save grade, enrollments |
src/docere/integrations/lms/canvas.py |
Canvas adapter: same interface |
src/docere/core/memory/memory_layer.py |
integrate_grade() — grade → memory record → concept mastery |
frontend/src/components/GradebookSyncModal.tsx |
4-step wizard UI (source → destination → validate → sync) |
frontend/src/api.ts |
Frontend API client functions for all gradebook endpoints |
LearningAnalyticsEvent— Generic event log (interactions, grade changes, logins) indexed by student, course, and event typeAlert— Instructor notifications with severity levels, evidence, and recommended actions (struggling student, breakthrough, class-wide pattern)StudyConfig— Per-course ablation study configuration with randomization seed and group definitionsResearchEvent— Audit log of all research-relevant events (group assignments, feature flag changes)
Users ──< Enrollments >── Courses
│ │
│ ├──< Assignments ──< Submissions
│ ├──< CourseMaterials
│ └──< StudyConfig ──< ResearchEvents
│
├──< Conversations ──< Messages ──< InteractionScores
│ │
│ └── strategy_id ──> Strategies ──< StrategyScores
│
├──< StudentProfiles
├──< ConceptMastery
├──< MemoryRecords
└──< LearningAnalyticsEvents
| Table | Purpose | Key Fields |
|---|---|---|
users |
Students, instructors, admins (auto-created from LMS) | external_lms_id, lms_platform, role |
courses |
Synced from Canvas/Moodle | syllabus_text, lms_sync_enabled, last_synced_at |
enrollments |
Student ↔ course with study group | study_group (control/treatment_memory/treatment_full) |
conversations |
Tutoring sessions | student_id, course_id, strategy_id, study_group |
messages |
Individual messages with embedding IDs | role, content, metadata (strategy info, memory tokens) |
memory_records |
Captured interactions, grades, insights | memory_type, concepts[], confusion_score, is_compressed |
student_profiles |
Aggregate per-student-per-course profile | engagement_level, current_grade, profile_summary |
concept_mastery |
Per-concept skill tracking | mastery_level, times_practiced, times_struggled |
interaction_scores |
Process verification scores | helpfulness, clarity, engagement, understanding_delta, composite_score |
strategies |
Teaching strategy archive with evolution lineage | prompt_template, avg_score, parent_strategy_id, generation |
strategy_scores |
Per-interaction strategy outcomes | score, context_metadata |
study_config |
Ablation study parameters | groups, randomization_seed, is_active |
research_events |
Audit log for research | event_type, event_data |
alerts |
Instructor notifications | alert_type, severity, recommended_action |
learning_analytics_events |
Generic analytics events | event_type, event_data |
All tables use UUID primary keys and timezone-aware timestamps.
Base URL: /api/v1/
| Method | Endpoint | Description |
|---|---|---|
POST |
/chat/conversations |
Start a new tutoring session |
GET |
/chat/conversations |
List student's conversations (filterable by course) |
GET |
/chat/conversations/{id} |
Get conversation with full message history |
POST |
/chat/conversations/{id}/messages |
Send message, receive AI tutoring response |
POST /chat/conversations/{id}/messages — The main tutoring endpoint:
Request:
{
"content": "I don't understand how recursion works"
}Response:
{
"message": {
"id": "uuid",
"role": "assistant",
"content": "Let's start with something you already know...",
"model_used": "claude-sonnet-4-20250514",
"token_count": 256,
"created_at": "2025-01-15T10:30:00Z"
},
"strategy_used": "Analogy-First",
"memory_context_tokens": 1847
}| Route Group | Endpoints | Status |
|---|---|---|
/auth |
JWT-based authentication | Placeholder |
/courses |
Course list, detail, sync triggers | Placeholder |
/students |
Student profiles, concept mastery | Placeholder |
/instructor |
Dashboard, at-risk alerts, class patterns | Placeholder |
/memory |
Memory retrieval, management | Placeholder |
/analytics |
Course metrics, strategy performance, research export | Placeholder |
/lms |
Sync hooks, webhook receivers | Placeholder |
GET /health → {"status": "ok", "version": "0.1.0"}
docere-v2/
├── src/docere/
│ ├── main.py # FastAPI app entry point, router registration
│ ├── config.py # Pydantic settings (env vars)
│ ├── dependencies.py # Dependency injection (DB sessions)
│ │
│ ├── api/ # Route handlers
│ │ ├── chat.py # Tutoring conversation endpoints (implemented)
│ │ ├── auth.py # Authentication (placeholder)
│ │ ├── courses.py # Course management (placeholder)
│ │ ├── students.py # Student profiles (placeholder)
│ │ ├── instructor.py # Instructor dashboard (placeholder)
│ │ ├── memory.py # Memory management (placeholder)
│ │ ├── analytics.py # Analytics & research export (placeholder)
│ │ └── lms.py # LMS sync hooks (placeholder)
│ │
│ ├── core/ # Core agent logic
│ │ ├── agent.py # TutoringAgent orchestrator
│ │ ├── memory/
│ │ │ ├── memory_layer.py # Three-source context assembly
│ │ │ ├── student_profile.py # Profile builder & narrative generation
│ │ │ ├── interaction_store.py # Qdrant interaction storage & diversity retrieval
│ │ │ ├── teacher_context.py # Course material ingestion & compression
│ │ │ └── compressor.py # Memory compression (CARTRIDGES-inspired)
│ │ ├── verification/
│ │ │ ├── process_verifier.py # Hybrid LLM + heuristic interaction scoring
│ │ │ ├── interaction_scorer.py # Heuristic scoring (clarity, engagement, quality)
│ │ │ └── outcome_tracker.py # Links interaction scores to LMS grade outcomes
│ │ └── improvement/
│ │ ├── strategy_archive.py # UCB1 bandit strategy selection
│ │ ├── strategy_evolver.py # Weekly mutation + pruning cycle
│ │ └── experiment_runner.py # Ablation study management & feature gating
│ │
│ ├── integrations/ # External service adapters
│ │ ├── llm/
│ │ │ ├── client.py # Anthropic Claude API wrapper
│ │ │ └── embeddings.py # Voyage AI / OpenAI embeddings with LRU cache
│ │ ├── lms/
│ │ │ ├── base.py # Abstract LMS adapter interface
│ │ │ ├── canvas.py # Canvas REST API adapter
│ │ │ └── moodle.py # Moodle API adapter
│ │ └── vector_db/
│ │ └── qdrant.py # Qdrant vector DB client
│ │
│ ├── models/ # SQLAlchemy ORM models
│ │ ├── base.py # Base class, UUID + timestamp mixins
│ │ ├── user.py # User (auto-created from LMS)
│ │ ├── course.py # Course, Enrollment, Assignment, Submission, CourseMaterial
│ │ ├── conversation.py # Conversation, Message
│ │ ├── memory.py # MemoryRecord, StudentProfile, ConceptMastery
│ │ ├── verification.py # InteractionScore
│ │ ├── strategy.py # Strategy, StrategyScore
│ │ ├── research.py # StudyConfig, ResearchEvent
│ │ ├── alert.py # Instructor alerts
│ │ └── analytics.py # LearningAnalyticsEvent
│ │
│ ├── schemas/ # Pydantic request/response schemas
│ │ └── chat.py # Chat endpoint schemas
│ │
│ ├── services/ # Business logic
│ │ └── lms_sync_service.py # Full + incremental LMS sync
│ │
│ ├── tasks/ # Background jobs (ARQ)
│ │ └── lms_sync.py # Periodic course sync (every 2 hours)
│ │
│ └── utils/ # Helpers
│
├── frontend/ # React + TypeScript SPA
│ └── src/
│ ├── App.tsx
│ ├── pages/ChatPage.tsx
│ ├── components/
│ │ ├── ClaudeChatInput.tsx
│ │ ├── MessageBubble.tsx
│ │ ├── CourseSelector.tsx
│ │ ├── Sidebar.tsx
│ │ └── ThemeToggle.tsx
│ └── hooks/useTheme.ts
│
├── alembic/ # Database migrations
│ ├── env.py
│ └── versions/
│
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── docker-compose.yml # Local dev (PostgreSQL, Redis, Qdrant)
├── pyproject.toml # Python dependencies & tool config
├── alembic.ini # Migration config
└── .env.example # Environment template
- Python 3.12+
- Docker & Docker Compose
- An Anthropic API key
- A Voyage AI or OpenAI API key (for embeddings)
git clone https://github.com/Koding-4-Kids/docere-v2.git
cd docere-v2python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"docker compose up -dThis starts:
- PostgreSQL 16 on port 5432
- Redis 7 on port 6379
- Qdrant on port 6333 (HTTP) and 6334 (gRPC)
cp .env.example .envEdit .env and set:
ANTHROPIC_API_KEY— your Anthropic keyVOYAGE_API_KEY— your Voyage AI key (orOPENAI_API_KEYfor OpenAI embeddings)- For LMS integration:
CANVAS_BASE_URL,CANVAS_API_TOKEN(or Moodle equivalents)
alembic upgrade headuvicorn docere.main:app --reload --host 0.0.0.0 --port 8000API docs available at http://localhost:8000/docs
cd frontend
npm install
npm run devAll configuration is via environment variables (loaded from .env):
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql+asyncpg://docere:docere_dev@localhost:5432/docere |
PostgreSQL connection |
REDIS_URL |
redis://localhost:6379 |
Redis connection |
QDRANT_URL |
http://localhost:6333 |
Qdrant vector DB |
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
— | Anthropic API key (required) |
DEFAULT_MODEL |
claude-sonnet-4-20250514 |
Claude model for tutoring |
VOYAGE_API_KEY |
— | Voyage AI embeddings (preferred) |
OPENAI_API_KEY |
— | OpenAI embeddings (fallback) |
EMBEDDING_MODEL |
voyage-3 |
Embedding model name |
EMBEDDING_DIMENSIONS |
1024 |
Vector dimensions |
| Variable | Default | Description |
|---|---|---|
MEMORY_SIMILARITY_THRESHOLD |
0.60 |
Min cosine similarity for memory retrieval |
MEMORY_DIVERSITY_THRESHOLD |
0.65 |
Max similarity between selected memories |
MEMORY_MAX_CONTEXT_TOKENS |
4000 |
Token budget for memory context |
MEMORY_COMPRESSION_THRESHOLD |
50 |
Compress after N uncompressed memories |
| Variable | Default | Description |
|---|---|---|
VERIFICATION_HELPFULNESS_WEIGHT |
0.35 |
Helpfulness weight in composite score |
VERIFICATION_CLARITY_WEIGHT |
0.25 |
Clarity weight |
VERIFICATION_UNDERSTANDING_WEIGHT |
0.25 |
Understanding delta weight |
VERIFICATION_ENGAGEMENT_WEIGHT |
0.15 |
Engagement weight |
VERIFICATION_FOLLOWUP_TIMEOUT_SECONDS |
300 |
Seconds to wait for student followup |
| Variable | Default | Description |
|---|---|---|
STRATEGY_MIN_USES_FOR_PRUNE |
20 |
Min uses before a strategy can be pruned |
STRATEGY_PRUNE_SCORE_THRESHOLD |
0.3 |
Prune strategies scoring below this |
STRATEGY_EVOLUTION_TOP_K |
3 |
Number of top strategies to mutate |
| Variable | Default | Description |
|---|---|---|
CANVAS_BASE_URL |
— | Canvas instance URL |
CANVAS_API_TOKEN |
— | Canvas API token |
MOODLE_BASE_URL |
— | Moodle instance URL |
MOODLE_API_TOKEN |
— | Moodle API token |
JWT_SECRET |
change-this-in-production |
JWT signing secret |
JWT_ALGORITHM |
HS256 |
JWT algorithm |
JWT_EXPIRATION_MINUTES |
1440 |
Token expiration (24 hours) |
LTI_ISSUER |
— | LTI 1.3 issuer URL |
LTI_CLIENT_ID |
— | LTI 1.3 client ID |
Docere v2 is designed for rigorous empirical evaluation via a three-arm ablation study:
┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────────────┐
│ Control │ │ Treatment: Memory │ │ Treatment: Full │
│ │ │ │ │ │
│ Base LLM only │ │ + Semantic memory │ │ + Semantic memory │
│ No memory │ │ + Student profiles │ │ + Student profiles │
│ No strategies │ │ + Context retrieval │ │ + Context retrieval │
│ No scoring │ │ No strategies │ │ + UCB1 strategies │
│ │ │ No scoring │ │ + Process verification │
│ │ │ │ │ + Strategy evolution │
└─────────────────┘ └─────────────────────┘ └─────────────────────────┘
Per interaction:
- Helpfulness, clarity, engagement, understanding delta (scored)
- Strategy used, memory context size
- Time to student followup
- Student followup classification (understood / still_confused / new_question / off_topic)
Per student:
- Engagement level trajectory
- Concept mastery over time
- Grade correlation with tutoring usage
Per strategy:
- Total uses, average score, success rate
- Evolution lineage and generation performance
- Best/worst performing interaction contexts
- Process verification for tutoring: Adapting process reward models (MATH-SHEPHERD) from math verification to open-ended educational dialogues
- Self-improvement from outcomes: Teaching strategies that evolve based on measured educational effectiveness, not just user satisfaction
- Core tutoring pipeline (memory → strategy → LLM → response)
- Three-source semantic memory with token budgeting
- Process verification (LLM-as-judge + heuristic hybrid)
- UCB1 strategy selection with 5 seed strategies
- Strategy evolution (mutation + pruning)
- Student profile builder with narrative summaries
- Concept mastery tracking
- Memory compression (CARTRIDGES-inspired)
- LMS adapter interface (Canvas/Moodle)
- LMS sync service (full + incremental)
- Outcome tracker (grade → interaction linking)
- Ablation study infrastructure (feature gating, deterministic assignment)
- Database models and schema
- Chat API endpoints
- Frontend skeleton (React + TypeScript)
- JWT authentication
- Instructor dashboard & analytics API
- At-risk student alerts
- LangGraph agent upgrade (tutoring + classroom graphs)
- All 18 backend endpoint stubs filled
- Canvas/Moodle webhook receivers
- Database migrations (Alembic)
- Rate limiting & production security
- Anonymized data export for research
- CI/CD pipeline (GitHub Actions)
- Frontend ↔ API integration
- LTI 1.3 launch flow
- SSE streaming for tutoring responses
- Gradebook sync (Excel / Google Sheets → LMS)
- Google Calendar integration (OAuth)
- Flashcard spaced repetition (FSRS)
- Classroom agent (instructor meta-queries)
- Background task scheduling (ARQ)
| Layer | Technology |
|---|---|
| Backend framework | FastAPI (async) |
| Database | PostgreSQL 16 (SQLAlchemy 2.0 async) |
| Vector database | Qdrant |
| Cache / task queue | Redis 7 + ARQ |
| LLM | Anthropic Claude |
| Embeddings | Voyage AI (primary), OpenAI (fallback) |
| Frontend | React 19, TypeScript, Vite, Tailwind CSS |
| Migrations | Alembic |
| Type checking | mypy (strict mode) |
| Linting | Ruff |
| Testing | pytest + pytest-asyncio |
| Containerization | Docker Compose |
MIT