feat(course-plan): RAG sources + multi-modal media + assignments + student view
Builds the §24 product on top of the LangGraph runtime from §22:
Phase A (Sources / RAG)
- encoach.course.plan.source model (file | url | text)
- SourceIndexer extracts PDF (pypdf), DOCX (python-docx), HTML, plain
text and embeds chunks via the existing pgvector pipeline scoped to
plan_id, so resources.search only returns the plan's own corpus
- Endpoints: list/create/upload/reindex/delete + plan-scoped retrieval
Phase B (Deliverables)
- services.deliverables.compute_deliverables walks the plan, derives
{planned, generated, ready} per week from material + media state
- GET /api/ai/course-plan/<id>/deliverables drives the new wizard
preview step and the live progress strip on the detail page
Phase C (Multi-modal media)
- encoach.course.plan.media model + MediaService:
audio: AWS Polly (default) or ElevenLabs
image: OpenAI DALL-E 3, capped per plan via system parameter
video: local ffmpeg subprocess (image + audio -> MP4 1280x720)
- Three new agent tools (media.synthesize_audio / generate_image /
compose_video), wired into course_week_materials and a new
course_media_director agent
- Endpoints per material + week-level batch generator
Phase D (Assignments)
- encoach.course.plan.assignment supports mode='batch' (op.batch) or
mode='students' (res.users), with due_date + message + state
- REST endpoints to list / create / delete assignments
Phase E (Student view)
- /api/student/course-plans + /api/student/course-plans/<id>
enforce visibility via assignment.expand_user_ids()
- New /student/course-plans list + read-only drilldown rendering
audio/image/video tiles from /web/content/<attachment_id>
Cross-cutting
- encoach.ai.tool.category: + media (so the new tools register)
- encoach.embedding gains a plan_id filter for plan-scoped RAG
- Wizard adds Sources + Multimedia steps; AdminCoursePlanDetail
rewritten with DeliverablesStrip + SourcesCard + AssignmentsCard +
per-material MediaDrawer
- ~280 new EN + AR i18n keys (full RTL coverage)
- smoke_course_plan.py exercises every phase via odoo-bin shell;
last run: PASS A/B/D/E + DALL-E 3 image (753 KB), Polly audio
fails cleanly when AWS creds aren't configured (expected)
Documentation: §24 added to docs/PROJECT_SUMMARY.md with phase-by-phase
artefact list, endpoints, smoke test, ops notes, and gotchas.
Made-with: Cursor
This commit is contained in:
@@ -1720,3 +1720,151 @@ docs/ENCOACH_FULL_DEMO_QA_REPORT.md # full QA write-up: credentials, dataset s
|
||||
- `khalid@encoach.test` was failing login because his password had drifted during earlier interactive testing. `reset_demo_passwords.py` now restores all canonical passwords idempotently.
|
||||
- `encoach.ai.log` field names differ from a naive guess — the model uses `service`, `action`, `model_used`, `prompt_tokens`, `completion_tokens`, `total_tokens`, `input_preview`, `output_preview`. `encoach.ai.feedback` uses `subject_type`, `subject_id`, `rating in {'up','down'}` (NOT `'thumbs_up'`). The seeder now matches both definitions.
|
||||
- The exam approval *post-approval* hook only fires when the **final** stage approves, so step 3 above advances the request without publishing the exam yet — that's correct behaviour, just not obvious from the API alone. The DB-side verification in step 6 is what makes it observable.
|
||||
|
||||
## 24. AI Course-Plans — RAG, Multi-modal Media, Assignments (2026-04-25)
|
||||
|
||||
§22 made LangGraph the runtime; §24 puts a real product on top of it. The user asked for an AI-driven **course-plan** experience modelled on the GE1 outline: detect deliverables up-front (e.g. "12 reading texts, 12 listening scripts, 24 audio narrations"), let admins drop reference files / URLs / pasted notes the AI must respect (RAG), generate multi-modal training material (images via DALL-E 3, voice via AWS Polly / ElevenLabs, slideshow video via local `ffmpeg`), then **assign** the finished plan to a class or to specific students. This section is the implementation log.
|
||||
|
||||
### 24.1 What this section delivers
|
||||
|
||||
| Phase | Capability | Where it lives |
|
||||
|---|---|---|
|
||||
| **A** | Reference sources (file/URL/text) → pgvector, scoped to plan | `encoach.course.plan.source` + `services/source_indexer.py` |
|
||||
| **B** | Deliverables preview & live progress strip | `services/deliverables.py` + `/api/ai/course-plan/<id>/deliverables` |
|
||||
| **C** | On-demand audio / image / video per material + bulk-per-week | `encoach.course.plan.media` + `services/media_service.py` |
|
||||
| **D** | Assign a plan to an `op.batch` or specific `res.users` | `encoach.course.plan.assignment` |
|
||||
| **E** | Student-side list + drilldown (read-only) | `/api/student/course-plans*` + `/student/course-plans` page |
|
||||
| **i18n** | Full EN + AR strings for every new control | `frontend/src/i18n/locales/{en,ar}.ts` |
|
||||
|
||||
### 24.2 New / extended backend artefacts
|
||||
|
||||
```text
|
||||
backend/custom_addons/encoach_ai_course/
|
||||
├── models/
|
||||
│ ├── course_plan_source.py ← NEW RAG ingestion row (file|url|text)
|
||||
│ ├── course_plan_media.py ← NEW audio|image|video asset
|
||||
│ └── course_plan_assignment.py ← NEW visibility unit (batch|students)
|
||||
├── services/
|
||||
│ ├── source_indexer.py ← NEW PDF/DOCX/HTML extraction → pgvector
|
||||
│ ├── deliverables.py ← NEW per-week status + percent_ready
|
||||
│ └── media_service.py ← NEW Polly/ElevenLabs/DALL-E/ffmpeg
|
||||
└── controllers/course_plan.py ← +18 endpoints (sources, media, assignments, student-side)
|
||||
```
|
||||
|
||||
Existing models updated:
|
||||
|
||||
* `encoach.course.plan` gains `source_ids`, `media_ids`, `assignment_ids`, `to_api_dict(include_media=...)`.
|
||||
* `encoach.ai.tool.category` selection extends with `('media', 'Media generation')` so the new tools register cleanly.
|
||||
* `encoach.embedding` queries gain a `plan_id` filter — `resources.search` is now plan-scoped when passed `plan_id`, so the agent sees only its own corpus.
|
||||
|
||||
New tools (registered in `encoach_ai/data/agents_defaults.xml`):
|
||||
|
||||
| Tool key | Provider | Notes |
|
||||
|---|---|---|
|
||||
| `media.synthesize_audio` | AWS Polly (default) / ElevenLabs | Used for `listening_script` + `speaking_prompt` |
|
||||
| `media.generate_image` | OpenAI DALL-E 3 | Capped per plan via `encoach_ai_course.image_budget_per_plan` (default 60) |
|
||||
| `media.compose_video` | local `ffmpeg` | Auto-creates audio + image first if missing |
|
||||
|
||||
New agent: **`course_media_director`** — orchestrates per-week media batches; reuses `course_week_materials`'s tools plus the three media tools above. The base `course_week_materials` agent now also has `media.synthesize_audio` + `media.generate_image` in its tool set so a single LangGraph turn can emit the text material *and* its narration/illustration.
|
||||
|
||||
### 24.3 New API endpoints (all JWT-protected unless noted)
|
||||
|
||||
```text
|
||||
# Sources (Phase A)
|
||||
POST /api/ai/course-plan/<id>/sources create URL or text source
|
||||
POST /api/ai/course-plan/<id>/sources/upload multipart file upload
|
||||
GET /api/ai/course-plan/<id>/sources list sources for plan
|
||||
POST /api/ai/course-plan/sources/<sid>/reindex re-extract + re-embed
|
||||
DELETE /api/ai/course-plan/sources/<sid> cascade-deletes embeddings
|
||||
|
||||
# Deliverables (Phase B)
|
||||
GET /api/ai/course-plan/<id>/deliverables {summary, weeks: [...]}
|
||||
|
||||
# Media (Phase C)
|
||||
GET /api/ai/course-plan/material/<mid>/media list media for material
|
||||
POST /api/ai/course-plan/material/<mid>/media/audio generate audio
|
||||
POST /api/ai/course-plan/material/<mid>/media/image generate image
|
||||
POST /api/ai/course-plan/material/<mid>/media/video compose video
|
||||
DELETE /api/ai/course-plan/media/<media_id> delete media + attachment
|
||||
POST /api/ai/course-plan/<id>/week/<n>/media bulk-generate week (audio+image)
|
||||
|
||||
# Assignments (Phase D)
|
||||
GET /api/ai/course-plan/<id>/assignments
|
||||
POST /api/ai/course-plan/<id>/assignments
|
||||
DELETE /api/ai/course-plan/assignments/<aid>
|
||||
|
||||
# Student-side (Phase E)
|
||||
GET /api/student/course-plans list assigned plans
|
||||
GET /api/student/course-plans/<id> read-only drilldown w/ media URLs
|
||||
```
|
||||
|
||||
### 24.4 New / updated frontend surfaces
|
||||
|
||||
* **Wizard** (`pages/admin/wizards/CoursePlanWizard.tsx`): now 6 steps — Brief → Resources (source builder) → Deliverables preview → Multimedia toggles → Review → Generate. Sources are queued client-side and uploaded post-creation against the new plan id.
|
||||
* **Detail page** (`pages/admin/AdminCoursePlanDetail.tsx`): now shows `DeliverablesStrip`, `SourcesCard`, `AssignmentsCard` (with `AssignDialog`), and per-material `MediaDrawer` (audio/image/video preview, generate, download, delete). Plus a "Generate week media" bulk action per week.
|
||||
* **Student** (`pages/student/StudentCoursePlans.tsx`, `StudentCoursePlanDetail.tsx`): list + read-only drilldown, `<audio>` / `<img>` / `<video>` tiles fed from `/web/content/<attachment_id>`.
|
||||
* **Sidebar**: new "My Course Plans" entry under student nav (`StudentLayout.tsx`).
|
||||
* **i18n**: ~140 new keys in EN + AR across `coursePlan.wizard.*`, `coursePlan.sources.*`, `coursePlan.deliverables.*`, `coursePlan.media.*`, `coursePlan.assignments.*`, `coursePlan.student.*`, `nav.myCoursePlans`.
|
||||
|
||||
### 24.5 Smoke test (run via `odoo-bin shell`)
|
||||
|
||||
`smoke_course_plan.py` exercises every phase end-to-end:
|
||||
|
||||
| Phase | Result |
|
||||
|---|---|
|
||||
| A — index inline-text source | PASS · 1 chunk · 295 chars (uses local MiniLM embeddings) |
|
||||
| B — `compute_deliverables` | PASS · 12 weeks · 5 ready / 1 generated / 0 planned |
|
||||
| C — DALL-E 3 image | PASS · 753 KB PNG generated for a `reading_text` material |
|
||||
| C — Polly audio | Gracefully marked `failed` with `"AWS credentials not configured — set in AI Settings"` (expected on dev box) |
|
||||
| D — assignment | PASS · created `mode='students'` assignment for Sarah |
|
||||
| E — visibility | PASS · `expand_user_ids()` returns Sarah · plan in student listing |
|
||||
|
||||
Run it any time:
|
||||
|
||||
```bash
|
||||
.conda-envs/odoo19/bin/python odoo/odoo-bin shell \
|
||||
-c odoo.conf -d encoach_v2 --no-http \
|
||||
< smoke_course_plan.py
|
||||
```
|
||||
|
||||
### 24.6 Operational notes
|
||||
|
||||
* **External providers are optional**. AWS Polly / ElevenLabs / OpenAI image keys live in **AI Settings** (`encoach.ai.settings`); when missing the corresponding media row is created with `status='failed'` and a clear error — the rest of the flow (planning, RAG, deliverables, assignment) still works.
|
||||
* **`ffmpeg`** must be on `PATH` for video composition. If it's missing, the video media row is `failed` with a one-line install hint; nothing else breaks.
|
||||
* **Image budget** per plan is the system parameter `encoach_ai_course.image_budget_per_plan` (default 60). Bump it via Settings → Technical → System Parameters.
|
||||
* **Plan-scoped retrieval**. The `resources.search` tool now accepts a `plan_id`; the LLM is auto-given the plan id in the system prompt of `course_planner` and `course_week_materials`, so RAG only ever sees that plan's own sources.
|
||||
* **Cascade behaviour**. Deleting a source unlinks its `encoach.embedding` rows; deleting a plan unlinks sources, media, assignments, and the underlying `ir.attachment`s.
|
||||
|
||||
### 24.7 Files added / changed in this pass
|
||||
|
||||
```text
|
||||
backend/custom_addons/encoach_ai_course/models/course_plan_source.py + new
|
||||
backend/custom_addons/encoach_ai_course/models/course_plan_media.py + new
|
||||
backend/custom_addons/encoach_ai_course/models/course_plan_assignment.py + new
|
||||
backend/custom_addons/encoach_ai_course/services/source_indexer.py + new
|
||||
backend/custom_addons/encoach_ai_course/services/deliverables.py + new
|
||||
backend/custom_addons/encoach_ai_course/services/media_service.py + new
|
||||
backend/custom_addons/encoach_ai_course/controllers/course_plan.py ~ +480 LOC
|
||||
backend/custom_addons/encoach_ai_course/security/ir.model.access.csv ~ access rights
|
||||
backend/custom_addons/encoach_ai/models/ai_agent.py ~ TOOL_CATEGORIES += media
|
||||
backend/custom_addons/encoach_ai/data/agents_defaults.xml ~ + 3 tools, + 1 agent
|
||||
backend/custom_addons/encoach_vector/models/embedding.py ~ plan_id filter
|
||||
frontend/src/types/coursePlan.ts ~ +4 interfaces
|
||||
frontend/src/services/coursePlan.service.ts ~ +18 methods
|
||||
frontend/src/pages/admin/wizards/CoursePlanWizard.tsx ~ +SourcesStep, MediaStep
|
||||
frontend/src/pages/admin/AdminCoursePlanDetail.tsx ~ rewrite (Deliverables, Sources, Assignments, MediaDrawer)
|
||||
frontend/src/pages/student/StudentCoursePlans.tsx + new
|
||||
frontend/src/pages/student/StudentCoursePlanDetail.tsx + new
|
||||
frontend/src/App.tsx ~ student routes
|
||||
frontend/src/components/StudentLayout.tsx ~ My Course Plans link
|
||||
frontend/src/i18n/locales/en.ts ~ +140 keys
|
||||
frontend/src/i18n/locales/ar.ts ~ +140 keys
|
||||
smoke_course_plan.py + new (E2E smoke test)
|
||||
```
|
||||
|
||||
### 24.8 Gotchas resolved during this pass
|
||||
|
||||
* `encoach.ai.tool.category` is a strict `Selection`; adding the three media tools required adding `('media', 'Media generation')` to `TOOL_CATEGORIES` in `ai_agent.py`. Without that the data file refused to load and the whole `encoach_ai` module rolled back its upgrade — silently leaving the new course-plan tables uncreated until we restarted with `-u encoach_vector,encoach_ai,encoach_ai_course`.
|
||||
* Odoo XML data loaders refuse two `<record id="..."/>` with the same id in the same file (no implicit merge). The `course_week_materials` agent had to be edited *in place* with the new media tools instead of being patched as a second record at the bottom.
|
||||
* `pypdf` extracts most encrypted-but-public PDFs; if both `pypdf` and `PyPDF2` are missing the indexer marks the source `failed` with a clear pip hint rather than crashing.
|
||||
* The student-side endpoint uses `expand_user_ids()` so a `mode='batch'` assignment auto-includes any future student rolled into that batch — no need to re-assign per intake.
|
||||
|
||||
Reference in New Issue
Block a user