- **Plan Mode** — AI analyzes input (document or prompt) and proposes a course structure. The teacher reviews, edits, and approves before anything is created.
- **Agent Mode** — AI executes approved actions: creating sections, lessons, exercises, and populating content. Shows real-time progress.
The client already has course content loaded in Svelte stores when the teacher is on a page. The agent:
- **Reads context from the client** — current lesson text, exercise questions, course structure are passed with each message. No extra API calls to "read" content.
- **Writes through the API** — all mutations route through existing service functions with the full middleware stack (`authMiddleware` -> `orgMemberMiddleware` -> `courseTeamMemberMiddleware`). Permission enforcement, single source of truth, and auditability for free.
- **Previews before bulk changes** — Plan Mode shows the full proposed structure before creating anything. For individual edits, the agent presents changes for review before applying.
The LLM sees a set of **tools** (mapped to existing service functions) and decides which to call based on the teacher's message. The agent API executes the tool with full permission checks, then feeds the result back to the LLM for a human-friendly summary.
Document upload is a premium feature — only available to orgs on the **EARLY_ADOPTER** plan or higher. BASIC (free) plan users cannot upload documents.
- The attachment icon in the chat input is visible to all teachers (so they discover the feature)
- When a BASIC plan user clicks it, the file picker does NOT open. Instead, the **upgrade modal** is triggered immediately (same `openUpgradeModal()` pattern used by certificates, team invites, etc.)
- For EARLY_ADOPTER+ users, the file picker opens normally
- Server validates file size before parsing. Files over 5MB are rejected with `413 { error: "file_too_large", maxSize: 5242880 }`
- Client validates before upload as well (check `file.size` on selection) and shows an inline error: "File is too large. Maximum size is 5MB."
- 5MB is sufficient for most textbook chapters, syllabi, and course outlines. Full textbook PDFs (50MB+) are out of scope — teachers should upload individual chapters.
### Upload Flow
1. Teacher clicks the attachment icon in the chat input
2.**Plan check**: if BASIC plan, trigger upgrade modal and stop. If EARLY_ADOPTER+, continue.
3. Selects a PDF or DOCX file
4.**Size check**: if file > 5MB, show inline error and stop
5. File is uploaded to `POST /agent/upload` which:
- Validates plan (EARLY_ADOPTER+ required)
- Validates file type (PDF/DOCX only) and size (max 5MB)
- Stores extracted text in Redis with a TTL (keyed by `documentId`, expires after 1 hour)
6. The `documentId` is attached to the next chat message
7. When the agent processes the message, it retrieves the full extracted text from Redis and includes it in the LLM context
### Text Extraction
**PDF**: Use `pdf-parse` (lightweight, pure JS, no native dependencies). Extracts text content from all pages. Handles multi-column layouts reasonably well for structured content like textbooks/syllabi.
**DOCX**: Use `mammoth` (pure JS). Extracts text content preserving heading structure (H1, H2, etc.) which helps the LLM understand document hierarchy for course structure generation.
### Why Server-Side Extraction
- Keeps document parsing out of the client bundle
- Text is extracted once and cached in Redis — subsequent agent turns reference the same extracted text without re-uploading
- The LLM never sees the raw binary file — only clean text, which is more token-efficient
- File validation and size limits are enforced server-side
The extracted text is NOT returned in the upload response (it can be very large). It lives in Redis and is fetched internally by the agent chat endpoint when `documentId` is provided in the chat message.
A thin, backend-focused package consumed by `apps/api`. No UI code. The heavy lifting is done by the Vercel AI SDK; this package provides project-specific configuration on top.
- **Provider factory** — creates the right AI SDK `LanguageModel` from env vars (`AI_PROVIDER`, `AI_API_KEY`, `AI_MODEL`)
- **Role-aware tool registry** — `getToolSchemas(role)` returns Zod schemas per role
- **Role-aware system prompt** — `buildSystemPrompt(context)` generates different LLM instructions for teachers vs students, including Plan Mode vs Agent Mode behavior
- **Course plan types** — `CoursePlan`, `CoursePlanSection`, `CoursePlanLesson` types used by both API and dashboard
The agent loop itself is NOT in this package — `streamText()` from `ai` core handles the full loop (prompt -> LLM -> tool call -> execute -> respond -> repeat). The Hono route assembles tools (schema from package + execute from services) and calls `streamText`.
-`'teacher'` — can read AND mutate course content (create sections, update lessons, generate questions, execute plans)
-`'student'` — can only read and discuss content (explain concepts, answer questions, quiz on material). Gated off in v1.
### Role detection — server-side, automatic
- API route checks user's course membership via existing `isCourseTeamMemberOrOrgAdmin`
- Team members (ADMIN/TUTOR role in `groupmember`) get `role: 'teacher'`
- Regular members (STUDENT role) get `role: 'student'`
- In v1, `role: 'student'` returns 403
### Tool registry — `getToolSchemas(role)`
-`shared` tools (read-only): available to both roles
-`teacher` tools (mutations): teacher only — includes section/lesson creation, content updates, plan execution
-`student` tools (v2): student only, stubs for now
Teachers get `shared + teacher`. Students get `shared + student`. Students can never access mutation tools, even via prompt injection.
### Single API endpoint
`/agent/chat` serves both roles. Role is detected server-side (not passed by the client, preventing spoofing). The endpoint loads tools and prompt based on the detected role.
| `create_exercise` | write | Create exercise with questions and options | `createExercise({ courseId, lessonId, sectionId, title, questions })` |
| `add_questions` | write | Add questions to an existing exercise | `updateExerciseService` (diff-based) |
| `generate_course_plan` | agent | Analyze text and return structured CoursePlan JSON | No service call — LLM generates the plan structure |
**Note on `generate_course_plan`**: This is a special tool. Unlike other tools that call services, this tool's "execution" is the LLM itself generating a structured JSON response conforming to the `CoursePlan` schema. The tool schema defines the output format so the LLM returns valid, parseable JSON rather than free-form text. The chat UI detects this tool's result and renders the plan view component.
- Cannot manage course members, invitations, or roles
- Cannot grade submissions or assign marks
- Cannot handle payments or attendance
- Cannot upload files, images, videos, or documents to lessons (the agent can receive documents for analysis, but cannot attach media to course content)
- Cannot delete sections, lessons, or exercises (v1 — added in Phase 2)
- Cannot reorder content (v1 — added in Phase 2)
- Cannot manage org settings, members, or billing
- Cannot access data from other courses or organizations
- Cannot browse the internet or fetch external URLs
- Cannot send emails or notifications
---
## Token-Based Usage Tracking
AI usage is metered by tokens. Each org gets a monthly base allowance from their plan, with the option to buy extra credit packs when exhausted.
### How It Works
1.**Before LLM call**: check `ai_token_usage` for current month + `ai_credit_balance`. If `remaining <= 0`, return 402.
2.**After LLM call**: `streamText()` result exposes `usage` with `promptTokens` and `completionTokens`. Insert row into `ai_token_usage`.
3.**Deduction logic**: monthly allowance consumed first. Credits only touched when `used_this_month > allowance`.
### Plan-Based Allowances
Tied to existing `organization_plan` table. Each plan gets a monthly token allowance and feature access:
| **BASIC** | 0 tokens | Not available | Not available — triggers upgrade modal |
| **EARLY_ADOPTER** | 500K tokens/month (~50-100 conversations) | Available | Available (max 5MB per file) |
| **ENTERPRISE** | 2M tokens/month (or unlimited) | Available | Available (max 5MB per file) |
Allowance resets on the 1st of each month.
Document upload is gated by plan, not by token balance. Even if an EARLY_ADOPTER has tokens remaining, a BASIC user still cannot upload. This is a feature gate, not a usage gate.
### Extra Credit Packs
- One-time purchases, do NOT expire, do NOT reset monthly
- Deduction order: monthly allowance first, then credits
2.**Tool-level permission checks** — tool set IS the security boundary. Students cannot call mutation tools. Each tool's execute function re-verifies resource ownership.
3.**Token budget enforcement** — every LLM call checks remaining balance. 402 if exhausted.
- **Cloud**: AI assistant enabled by default. ClassroomIO provides the LLM API key server-side. Usage metered per plan tier.
- **Self-hosted**: AI assistant **disabled by default**. To enable, admin sets `AI_API_KEY` and optionally `AI_PROVIDER` / `AI_MODEL` in env. When configured, assistant becomes available. When not configured, button is hidden. No token metering for self-hosted (user's own API key, their cost).
---
## UI Specification
### Trigger
"AI Assistant" button in the course sidebar navigation. In v1, only visible to teachers/admins. Hidden when self-hosted and no AI key configured.
### Panel
Right sidebar using `@cio/ui` Sheet (`side="right"`, ~420px). Does NOT replace the left sidebar — both can be open simultaneously.
### Chat Interface
- Message list with user/assistant turns
- Text input at bottom with send button and **attachment icon** (for PDF/DOCX upload)
- Streaming token display (ChatGPT-like typing)
- Quick-action chips (context-dependent)
- Stop button while streaming
### File Upload in Chat
- Attachment icon next to the text input (paperclip icon)
- **BASIC plan users**: clicking the attachment icon triggers the upgrade modal (`openUpgradeModal()`). File picker does not open. A subtle tooltip or label can hint "Upgrade to upload documents".
- **Client-side size check**: if selected file exceeds 5MB, show inline error below the input: "File is too large. Maximum size is 5MB." File is not uploaded.
- Selected file shows as a chip above the input: `[Biology-Textbook.pdf (24 pages)] [x]`
- On send, file is uploaded first (`POST /agent/upload`), then `documentId` is included in the chat message
- While uploading: chip shows spinner. While parsing: "Analyzing document..."
- If server returns 403 (`document_upload_requires_upgrade`): trigger upgrade modal
- After parsing: agent enters Plan Mode and proposes a course structure
### Plan View Component
When the agent returns a `generate_course_plan` tool result, the chat renders a **plan card** instead of plain text:
- "Ask for Changes" button -> focuses the text input with a prompt to describe changes
- Teacher can also just type feedback naturally: "Add a section on ecology"
### Agent Mode Progress
During plan execution, a progress card replaces the plan card:
```
+------------------------------------------+
| Implementing Course Plan |
| |
| [x] Section: Cell Biology |
| [x] Lesson: Cell Structure |
| [x] Content written (1,850 words) |
| [x] Exercise: 5 MCQ questions |
| [~] Lesson: Cell Division |
| [ ] Content |
| [ ] Exercise |
| [ ] Section: Genetics |
| [ ] Lesson: DNA and Gene Expression |
| [ ] Lesson: Heredity |
| |
| Creating lesson content... |
| [Stop] |
+------------------------------------------+
```
- Checkmarks for completed steps
- Spinner for in-progress step
- Empty circles for pending steps
- Stop button to halt execution (already-created content is kept)
### Role-Aware Quick Actions
Teacher (v1):
- "Upload a document to create a course"
- "Draft a lesson"
- "Generate questions from this lesson"
- "Improve this lesson's text"
Student (v2):
- "Explain this lesson"
- "Quiz me on this topic"
- "Help me with this exercise"
### Context Awareness
Client passes `courseId`, current `lessonId` or `exerciseId`. The role is NOT sent from the client — detected server-side. The system prompt includes: "The teacher is currently viewing lesson/exercise ID: {id}".
### Store Refresh
`onFinish` callback fires after the agent completes. Refreshes the relevant store:
- After plan execution: full course content tree refresh
- After lesson update: `lessonApi.get()` reload
- After exercise creation/update: exercise data reload
---
## Streaming
The Vercel AI SDK provides structured SSE via `toUIMessageStreamResponse()`:
- Text chunks (tokens stream one by one)
- Tool call events (when LLM decides to call a tool)
- Tool results (when tool execution completes)
- Finish signals
On the frontend, the `Chat` class from `@ai-sdk/svelte` consumes this protocol automatically. No manual SSE parsing.
---
## API Routes
New route domain: `/agent`
| Method | Route | Description | Auth |
|--------|-------|-------------|------|
| `GET` | `/agent/status` | Feature flag + role + usage info | auth + orgMember |