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:
Yamen Ahmad
2026-04-25 17:13:01 +04:00
parent 0ed7f88cab
commit 096b042daf
29 changed files with 4608 additions and 1586 deletions

View File

@@ -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.