Some checks failed
Deploy to Staging / Deploy backend + frontend to staging (push) Failing after 14s
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
182 lines
6.2 KiB
Python
182 lines
6.2 KiB
Python
"""End-to-end smoke test for the AI course-plan feature.
|
|
|
|
Run via: odoo-bin shell -c odoo.conf -d encoach_v2 < smoke_course_plan.py
|
|
|
|
Tests Phases A-E of the LangGraph-based course-plan pipeline:
|
|
A. Sources / RAG (encoach.course.plan.source + SourceIndexer)
|
|
B. Deliverables (services.deliverables.compute_deliverables)
|
|
C. Multimedia (encoach.course.plan.media + MediaService)
|
|
D. Assignments (encoach.course.plan.assignment)
|
|
E. Student visibility (assignment.expand_user_ids)
|
|
|
|
External-API steps (DALL-E, Polly) tolerate missing credentials by
|
|
checking ``status == 'failed'`` and reporting the provider error rather
|
|
than failing the whole script.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
|
|
|
|
def hr(title):
|
|
print(f"\n{'='*70}\n{title}\n{'='*70}")
|
|
|
|
|
|
def fail(msg):
|
|
print(f" FAIL {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
def ok(msg):
|
|
print(f" PASS {msg}")
|
|
|
|
|
|
hr("0. Locate fixtures")
|
|
Plan = env['encoach.course.plan'].sudo()
|
|
plan = Plan.search([], order='id desc', limit=1)
|
|
if not plan:
|
|
fail("No course plan in DB; seed one first")
|
|
print(f" using plan #{plan.id}: {plan.name!r} (CEFR {plan.cefr_level})")
|
|
|
|
Users = env['res.users'].sudo()
|
|
sarah = Users.search([('login', '=', 'sarah@encoach.test')], limit=1)
|
|
if not sarah:
|
|
fail("sarah@encoach.test not seeded")
|
|
print(f" using student user #{sarah.id}: {sarah.name}")
|
|
|
|
|
|
# ---------------------------------------------------------------- Phase A
|
|
hr("Phase A — Sources & RAG indexing")
|
|
|
|
Source = env['encoach.course.plan.source'].sudo()
|
|
src = Source.create({
|
|
'plan_id': plan.id,
|
|
'name': 'Smoke test inline source',
|
|
'kind': 'text',
|
|
'inline_text': (
|
|
'In Week 1 of the General English course we focus on present '
|
|
'simple and present continuous. Reading texts target daily '
|
|
'routines, family, and study habits at CEFR A2-B1 level. '
|
|
'Listening scripts feature 3-minute monologues on student life '
|
|
'with comprehension questions covering main idea and detail.'
|
|
),
|
|
'mime_type': 'text/plain',
|
|
})
|
|
ok(f"created source #{src.id}")
|
|
|
|
from odoo.addons.encoach_ai_course.services.source_indexer import (
|
|
SourceIndexer,
|
|
)
|
|
result = SourceIndexer(env).index(src)
|
|
print(f" index result: {result}")
|
|
src.invalidate_recordset()
|
|
if src.status == 'indexed' and src.chunks_count > 0:
|
|
ok(f"indexed: {src.chunks_count} chunks, {src.extracted_chars} chars")
|
|
elif src.status == 'failed':
|
|
print(f" WARN indexing failed (likely no embedding API key configured): "
|
|
f"{src.error}")
|
|
else:
|
|
print(f" WARN unexpected status: {src.status}")
|
|
|
|
env.cr.commit()
|
|
|
|
|
|
# ---------------------------------------------------------------- Phase B
|
|
hr("Phase B — Deliverables computation")
|
|
from odoo.addons.encoach_ai_course.services.deliverables import (
|
|
compute_deliverables,
|
|
)
|
|
deliv = compute_deliverables(plan)
|
|
summary = deliv['summary']
|
|
print(f" summary: total={summary['total']} planned={summary['planned']} "
|
|
f"generated={summary['generated']} ready={summary['ready']} "
|
|
f"%={summary['percent_ready']}")
|
|
print(f" media (current/ready): "
|
|
f"audio {summary['media']['audio']}/{summary['media']['audio_ready']} "
|
|
f"image {summary['media']['image']}/{summary['media']['image_ready']} "
|
|
f"video {summary['media']['video']}/{summary['media']['video_ready']}")
|
|
ok(f"compute_deliverables returned {len(deliv['weeks'])} weeks")
|
|
|
|
|
|
# ---------------------------------------------------------------- Phase C
|
|
hr("Phase C — Multimedia generation (audio + image)")
|
|
from odoo.addons.encoach_ai_course.services.media_service import MediaService
|
|
mat_listening = plan.material_ids.filtered(
|
|
lambda m: m.material_type == 'listening_script'
|
|
)[:1]
|
|
mat_reading = plan.material_ids.filtered(
|
|
lambda m: m.material_type == 'reading_text'
|
|
)[:1]
|
|
|
|
svc = MediaService(env)
|
|
if mat_listening:
|
|
audio_media = svc.synthesize_audio(mat_listening, language='en-GB')
|
|
print(f" audio media #{audio_media.id} status={audio_media.status} "
|
|
f"err={(audio_media.error or '')[:120]!r}")
|
|
if audio_media.status == 'ready':
|
|
ok(f"audio bytes={audio_media.size_bytes}")
|
|
else:
|
|
print(f" WARN audio not ready (provider may need creds)")
|
|
else:
|
|
print(" SKIP no listening_script material on plan")
|
|
|
|
if mat_reading:
|
|
image_media = svc.generate_image(mat_reading, size='1024x1024')
|
|
print(f" image media #{image_media.id} status={image_media.status} "
|
|
f"err={(image_media.error or '')[:120]!r}")
|
|
if image_media.status == 'ready':
|
|
ok(f"image bytes={image_media.size_bytes}")
|
|
else:
|
|
print(f" WARN image not ready (provider may need creds)")
|
|
else:
|
|
print(" SKIP no reading_text material on plan")
|
|
|
|
env.cr.commit()
|
|
|
|
|
|
# ---------------------------------------------------------------- Phase D
|
|
hr("Phase D — Assignment creation")
|
|
Assignment = env['encoach.course.plan.assignment'].sudo()
|
|
existing = Assignment.search([
|
|
('plan_id', '=', plan.id),
|
|
('mode', '=', 'students'),
|
|
('state', '=', 'active'),
|
|
])
|
|
existing.unlink()
|
|
asn = Assignment.create({
|
|
'plan_id': plan.id,
|
|
'mode': 'students',
|
|
'student_user_ids': [(6, 0, [sarah.id])],
|
|
'message': 'Smoke test assignment',
|
|
'state': 'active',
|
|
})
|
|
ok(f"created assignment #{asn.id} for {len(asn.student_user_ids)} student(s)")
|
|
print(f" to_api_dict: {json.dumps(asn.to_api_dict(), default=str)[:200]} ...")
|
|
env.cr.commit()
|
|
|
|
|
|
# ---------------------------------------------------------------- Phase E
|
|
hr("Phase E — Student visibility")
|
|
expanded = asn.expand_user_ids()
|
|
print(f" expand_user_ids -> {expanded}")
|
|
if sarah.id in expanded:
|
|
ok("sarah@encoach.test is included in assignment expansion")
|
|
else:
|
|
fail("sarah not visible in assignment.expand_user_ids()")
|
|
|
|
assignments = Assignment.search([
|
|
('state', '=', 'active'),
|
|
('student_user_ids', 'in', [sarah.id]),
|
|
])
|
|
visible_plans = {a.plan_id.id for a in assignments
|
|
if sarah.id in a.expand_user_ids()}
|
|
print(f" Sarah will see plans: {sorted(visible_plans)}")
|
|
if plan.id in visible_plans:
|
|
ok("Plan appears in student-side listing query")
|
|
else:
|
|
fail("Plan NOT in student-side listing")
|
|
|
|
|
|
hr("DONE — Course-plan smoke test passed (provider warnings are OK)")
|
|
print("Test source / assignment created. Review in admin UI on plan #%d." % plan.id)
|