Core AI runtime - New encoach.ai.agent + encoach.ai.tool models with M2M tool binding, graph topology (simple|plan_review_revise|rag|react), model + fallback, temperature, max_tokens, response_format, max_revisions, quality checks and system prompt fields. - services/agent_runtime.py compiles a langgraph.StateGraph per agent and caches the build per (key, write_date). Emits a structured trace (output, tool_calls, retrieval_hits, revisions, quality_issues, ms, model_used, fallback_used) and auto-falls-back on rate-limit/5xx. - services/agent_tools.py registers 11 tool handlers wrapping existing services: resources.search, rubric.fetch, outcomes.fetch, student.profile, quality.cefr_check, quality.ai_detect, quality.content_gate, course_plan.save (mutates), course_plan.save_materials (mutates), scoring.grade_writing, scoring.grade_speaking. - 7 default agents seeded via data/agents_defaults.xml: course_planner, course_week_materials, exam_generator, exercise_generator, lms_tutor, writing_grader, speaking_grader. - Feature flag encoach_ai.use_langgraph_runtime (default True). - encoach_ai_course pipeline now routes through AgentRuntime when on, legacy SDK path kept as fallback. Admin UI - /admin/ai/prompts is now a tabbed Agents | Tools | Prompts console. - AIAgentsPanel: card grid + config dialog (model/temp/graph/tools/ system prompt) + built-in Test Runner showing live trace. - AIToolsPanel: registry table with category badges, mutates flag, schema viewer, edit dialog. - New /api/ai/agents* and /api/ai/tools* controller (list/get/update/ test, list-tools, toggle-tool). - Sidebar label nav.aiPrompts -> nav.aiAgents (AI Agents and Tools). - EN + AR (RTL) translations for ~80 new keys. Smart Wizard pages - /admin/quick-setup hub + CourseWizard, CoursePlanWizard, RubricWizard, ExamStructureWizard step-by-step flows. - /admin/course-plans list + detail pages. - /teacher/quick-setup mirror. Full demo seed + 8-role E2E - seed_full_demo.py adds the 5 missing user_types (approver, corporate, mastercorporate, agent, developer), activates a 2-stage exam-approval workflow with one pending request, creates a GE1-aligned 12-week B1 course plan with 6 detailed Week-1 materials (reading 400w, writing, listening 4-min script, speaking, grammar present simple vs continuous, vocabulary), and inserts sample ai.log + ai.feedback rows. - reset_demo_passwords.py forces every demo login back to canonical passwords (admin123/teacher123/student123/approver123/corporate123/ master123/agent123/dev123). - e2e_full_scenario.py: 46/46 PASS read-only API smoke across all 8 roles, including a live LangGraph round-trip on writing_grader. - e2e_approval_chain.py: 6/6 PASS mutation E2E - approver approves stage 1, admin approves stage 2, linked encoach.exam.custom flips to status=published, verified via psql. Docs - docs/PROJECT_SUMMARY.md updated to 2026-04-25: new Latest events bullets, refreshed credentials table, full sections 22 (LangGraph runtime) and 23 (full demo seed + 8-role E2E). - docs/ENCOACH_FULL_DEMO_QA_REPORT.md added with credentials, per-endpoint PASS/FAIL, mutation chain proof, LangGraph live output. - backend/GE1 Course Outline_ Fall AY25-26.pdf vendored as the reference outline the GE1 plan/materials are aligned to. Dependencies - requirements.txt: langgraph>=0.2.0, langchain-core>=0.3.0. - encoach_ai/__manifest__.py: external_dependencies updated. Made-with: Cursor
581 lines
33 KiB
Python
581 lines
33 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
seed_full_demo.py — Idempotent demo data filler covering ALL user types.
|
||
|
||
Run AFTER `seed_demo.py`. Fills the gaps so every product surface has
|
||
believable data:
|
||
|
||
• Missing user types: corporate, master corporate, agent, developer, approver
|
||
• Active 2-stage approval workflow + one PENDING exam approval request
|
||
so the approver flow is testable end-to-end.
|
||
• A rich GE1-aligned B1 course plan (12 weeks) with detailed week 1
|
||
teaching materials (reading, writing, listening, speaking, grammar,
|
||
vocabulary) matching the UTAS GE1 outline the user shared.
|
||
• Sample writing + speaking submissions with AI grader output, so the
|
||
` writing_grader` / `speaking_grader` agents have telemetry.
|
||
• A few `encoach.ai.feedback` rows so the prompts page has activity.
|
||
|
||
Run inside Odoo shell:
|
||
|
||
cd /Users/yamenahmad/projects2026/odoo/odoo19
|
||
.conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf -d encoach_v2 < seed_full_demo.py
|
||
"""
|
||
|
||
import json
|
||
import time
|
||
from datetime import datetime, timedelta
|
||
|
||
print("\n" + "=" * 72)
|
||
print(" EnCoach — Full demo seed (idempotent)")
|
||
print("=" * 72)
|
||
|
||
Users = env['res.users']
|
||
Entity = env['encoach.entity']
|
||
|
||
# ── Resolve the demo entity (created by seed_demo.py). Fall back to first.
|
||
entity = Entity.search([('code', '=', 'DEMO_ACADEMY')], limit=1)
|
||
if not entity:
|
||
entity = Entity.search([], limit=1)
|
||
if not entity:
|
||
raise SystemExit("✗ No encoach.entity found. Run seed_demo.py first.")
|
||
print(f"✓ Using entity: {entity.name} (id={entity.id})")
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 1. Demo users — every product user_type
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
DEMO_USERS = [
|
||
# Existing (idempotent: will be skipped if already there)
|
||
{'name': 'Admin User', 'login': 'admin@encoach.test', 'password': 'admin123', 'user_type': 'admin'},
|
||
{'name': 'Sarah Ahmed', 'login': 'sarah@encoach.test', 'password': 'student123', 'user_type': 'student'},
|
||
{'name': 'Omar Khan', 'login': 'omar@encoach.test', 'password': 'student123', 'user_type': 'student'},
|
||
{'name': 'Layla Nasser', 'login': 'layla@encoach.test', 'password': 'student123', 'user_type': 'student'},
|
||
{'name': 'Dr. Khalid', 'login': 'khalid@encoach.test', 'password': 'teacher123', 'user_type': 'teacher'},
|
||
{'name': 'Ms. Fatima', 'login': 'fatima@encoach.test', 'password': 'teacher123', 'user_type': 'teacher'},
|
||
# NEW user types
|
||
{'name': 'Approver Coach', 'login': 'approver@encoach.test', 'password': 'approver123', 'user_type': 'teacher'},
|
||
{'name': 'Acme Corporate', 'login': 'corporate@encoach.test','password': 'corporate123', 'user_type': 'corporate'},
|
||
{'name': 'Master Group HQ', 'login': 'master@encoach.test', 'password': 'master123', 'user_type': 'mastercorporate'},
|
||
{'name': 'Sales Agent', 'login': 'agent@encoach.test', 'password': 'agent123', 'user_type': 'agent'},
|
||
{'name': 'Platform Dev', 'login': 'dev@encoach.test', 'password': 'dev123', 'user_type': 'developer'},
|
||
]
|
||
|
||
users_by_login = {}
|
||
created_count = 0
|
||
for u in DEMO_USERS:
|
||
existing = Users.search([('login', '=', u['login'])], limit=1)
|
||
if existing:
|
||
users_by_login[u['login']] = existing
|
||
continue
|
||
user = Users.with_context(no_reset_password=True).create({
|
||
'name': u['name'],
|
||
'login': u['login'],
|
||
'password': u['password'],
|
||
'email': u['login'],
|
||
'user_type': u['user_type'],
|
||
'account_status': 'activated',
|
||
'is_verified': True,
|
||
'entity_ids': [(4, entity.id)],
|
||
})
|
||
users_by_login[u['login']] = user
|
||
created_count += 1
|
||
env.cr.commit()
|
||
print(f"✓ Demo users: total={len(users_by_login)}, newly created={created_count}")
|
||
for login, rec in users_by_login.items():
|
||
print(f" • {login:<32} {rec.user_type:<16} id={rec.id}")
|
||
|
||
admin = users_by_login['admin@encoach.test']
|
||
khalid = users_by_login['khalid@encoach.test']
|
||
fatima = users_by_login['fatima@encoach.test']
|
||
approver = users_by_login['approver@encoach.test']
|
||
sarah = users_by_login['sarah@encoach.test']
|
||
omar = users_by_login['omar@encoach.test']
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 2. Approval workflow — activate + add the approver as stage 1
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
Workflow = env['encoach.approval.workflow']
|
||
Stage = env['encoach.approval.stage']
|
||
Request = env['encoach.approval.request']
|
||
|
||
wf = Workflow.search([('name', '=', 'Exam Approval Workflow')], limit=1)
|
||
if not wf:
|
||
wf = Workflow.create({
|
||
'name': 'Exam Approval Workflow',
|
||
'type': 'content',
|
||
'status': 'active',
|
||
'allow_bypass': False,
|
||
'entity_id': entity.id,
|
||
})
|
||
else:
|
||
wf.write({'status': 'active'})
|
||
env.cr.commit()
|
||
|
||
if not Stage.search([('workflow_id', '=', wf.id), ('approver_id', '=', approver.id)], limit=1):
|
||
Stage.create({
|
||
'workflow_id': wf.id,
|
||
'sequence': 10,
|
||
'approver_id': approver.id,
|
||
'max_days': 3,
|
||
'auto_escalate': False,
|
||
'status': 'pending',
|
||
})
|
||
if not Stage.search([('workflow_id', '=', wf.id), ('approver_id', '=', admin.id)], limit=1):
|
||
Stage.create({
|
||
'workflow_id': wf.id,
|
||
'sequence': 20,
|
||
'approver_id': admin.id,
|
||
'max_days': 3,
|
||
'auto_escalate': False,
|
||
'status': 'pending',
|
||
})
|
||
env.cr.commit()
|
||
print(f"✓ Approval workflow: {wf.name} (id={wf.id}, status={wf.status}, stages={len(wf.stage_ids)})")
|
||
|
||
# Hand a concrete pending exam to the approver if there isn't already one assigned to them.
|
||
ExamCustom = env['encoach.exam.custom']
|
||
pending_exams = ExamCustom.search([('status', '!=', 'approved')], limit=1)
|
||
target_exam = pending_exams[:1] or ExamCustom.search([], limit=1)
|
||
if target_exam:
|
||
pending_for_approver = Request.search([
|
||
('workflow_id', '=', wf.id),
|
||
('res_model', '=', 'encoach.exam.custom'),
|
||
('res_id', '=', target_exam.id),
|
||
('state', 'in', ('draft', 'in_progress')),
|
||
], limit=1)
|
||
if not pending_for_approver:
|
||
first_stage = wf.stage_ids.sorted('sequence')[:1]
|
||
Request.create({
|
||
'workflow_id': wf.id,
|
||
'res_model': 'encoach.exam.custom',
|
||
'res_id': target_exam.id,
|
||
'state': 'in_progress',
|
||
'requester_id': khalid.id,
|
||
'current_stage_id': first_stage.id if first_stage else False,
|
||
})
|
||
env.cr.commit()
|
||
print(f"✓ Pending approval request created for exam {target_exam.id} ({target_exam.title})")
|
||
else:
|
||
print(f"✓ Approval request already pending for exam {target_exam.id}")
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 3. GE1-aligned B1 course plan with rich week 1 materials
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
CoursePlan = env['encoach.course.plan']
|
||
CoursePlanWeek = env['encoach.course.plan.week']
|
||
CoursePlanMat = env['encoach.course.plan.material']
|
||
OpCourse = env['op.course']
|
||
|
||
course = OpCourse.search([('code', '=', 'GE1-B1')], limit=1)
|
||
if not course:
|
||
course = OpCourse.create({
|
||
'name': 'General English 1 (B1) — Demo',
|
||
'code': 'GE1-B1',
|
||
'evaluation_type': 'normal',
|
||
})
|
||
env.cr.commit()
|
||
|
||
plan = CoursePlan.search([('name', '=', 'GE1 — General English 1 (B1)')], limit=1)
|
||
ge1_objectives = [
|
||
'Comprehend level-appropriate texts of around 400 words, recognising main ideas and specific details.',
|
||
'Write phrases and simple/compound sentences using basic conjunctions to link ideas clearly.',
|
||
'Listen to dialogues or monologues of 3–4 minutes delivered in carefully articulated speech.',
|
||
'Use phrases and simple/compound sentences to describe people, places, and study/work-related activities.',
|
||
'Use core grammar (present simple, present continuous, past simple) accurately enough not to obscure meaning.',
|
||
]
|
||
ge1_outcomes = {
|
||
'reading': [
|
||
{'code': 'RLO1', 'description': 'Use pre-reading strategies to preview, activate prior knowledge, predict content and establish a purpose for reading.'},
|
||
{'code': 'RLO2', 'description': 'Comprehend level-appropriate texts of around 400 words recognising main ideas and specific details.'},
|
||
{'code': 'RLO3', 'description': 'Scan passages and texts (including visuals) to extract specific information.'},
|
||
{'code': 'RLO4', 'description': 'Use context clues to guess the meaning of unfamiliar words in reading texts.'},
|
||
{'code': 'RLO5', 'description': 'Demonstrate possession of a range of level-appropriate actively-understood vocabulary.'},
|
||
{'code': 'RLO6', 'description': 'Infer meaning from a reading text.'},
|
||
],
|
||
'writing': [
|
||
{'code': 'WLO1', 'description': 'Use pre-writing strategies to generate and develop ideas and make a plan before writing.'},
|
||
{'code': 'WLO2', 'description': 'Write phrases and simple/compound sentences using basic conjunctions to link ideas clearly.'},
|
||
{'code': 'WLO3', 'description': 'Write paragraphs forming a text of at least 150 words.'},
|
||
],
|
||
'listening': [
|
||
{'code': 'LLO1', 'description': 'Use pre-listening strategies to preview, activate prior knowledge, predict content, and identify keywords.'},
|
||
{'code': 'LLO2', 'description': 'Listen to a dialogue or monologue of 3–4 minutes delivered in carefully articulated speech.'},
|
||
{'code': 'LLO3', 'description': 'Understand clear standard speech related to personal, social, academic and work-related topics.'},
|
||
],
|
||
'speaking': [
|
||
{'code': 'SLO1', 'description': 'Use pre-speaking strategies to communicate successfully by activating prior knowledge.'},
|
||
{'code': 'SLO2', 'description': 'Use phrases and simple/compound sentences to describe people, places, and study/work-related activities.'},
|
||
{'code': 'SLO3', 'description': 'Maintain communication by expressing lack of understanding or asking for repetition.'},
|
||
],
|
||
'grammar': [
|
||
{'code': 'GLO1', 'description': 'Use present simple and present continuous accurately to describe routines and current actions.'},
|
||
{'code': 'GLO2', 'description': 'Use past simple to talk about completed past activities.'},
|
||
],
|
||
'vocabulary': [
|
||
{'code': 'VLO1', 'description': 'Demonstrate a level-appropriate active vocabulary covering personal, social, academic and work-related topics.'},
|
||
],
|
||
}
|
||
ge1_grammar = [
|
||
{'code': 'G1', 'label': 'Present simple', 'sub_items': ['routines', 'facts', 'time expressions: every day, on Mondays']},
|
||
{'code': 'G2', 'label': 'Present continuous', 'sub_items': ['now / at the moment', 'temporary actions']},
|
||
{'code': 'G3', 'label': 'Past simple — regular and irregular', 'sub_items': ['ago / yesterday / last week', 'common irregular verbs']},
|
||
]
|
||
ge1_assessment = {
|
||
'continuous_assessment': 60,
|
||
'final_examination': 40,
|
||
'breakdown': {
|
||
'reading_writing_progress_test': 15,
|
||
'listening_speaking_progress_test': 15,
|
||
'class_participation': 10,
|
||
'writing_portfolio': 10,
|
||
'speaking_interview': 10,
|
||
'final_exam': 40,
|
||
},
|
||
'notes': 'Skills are split: Reading & Writing (10 hrs/week) and Listening & Speaking (8 hrs/week).',
|
||
}
|
||
ge1_resources = [
|
||
{'type': 'textbook', 'title': 'New Headway Pre-Intermediate (Student Book + Workbook)'},
|
||
{'type': 'textbook', 'title': 'Q: Skills for Success Reading & Writing 2'},
|
||
{'type': 'platform', 'title': 'EnCoach LMS'},
|
||
{'type': 'web', 'title': 'British Council LearnEnglish — Pre-Intermediate'},
|
||
]
|
||
|
||
if not plan:
|
||
plan = CoursePlan.create({
|
||
'name': 'GE1 — General English 1 (B1)',
|
||
'course_id': course.id,
|
||
'cefr_level': 'b1',
|
||
'total_weeks': 12,
|
||
'contact_hours_per_week': 18,
|
||
'skills_division': '10 hrs/wk Reading & Writing + 8 hrs/wk Listening & Speaking',
|
||
'description': (
|
||
'A 12-week, 18-hour-per-week B1 General English course aligned to the '
|
||
'UTAS GE1 outline. Skills are taught on parallel tracks — Reading & '
|
||
'Writing and Listening & Speaking — with grammar woven through both.'
|
||
),
|
||
'objectives_json': json.dumps(ge1_objectives),
|
||
'outcomes_json': json.dumps(ge1_outcomes),
|
||
'grammar_json': json.dumps(ge1_grammar),
|
||
'assessment_json': json.dumps(ge1_assessment),
|
||
'resources_json': json.dumps(ge1_resources),
|
||
'status': 'approved',
|
||
})
|
||
env.cr.commit()
|
||
print(f"✓ Course plan: {plan.name} (id={plan.id}, status={plan.status})")
|
||
|
||
# Build a 12-week skeleton; week 1 also gets full materials.
|
||
WEEKS = [
|
||
(1, '8–12 Sep. 2025', 'Unit 1 — Getting to know you', 'Personal introductions, daily routines, present tenses'),
|
||
(2, '15–19 Sep. 2025', 'Unit 2 — The way we live', 'Habits and lifestyle, frequency adverbs'),
|
||
(3, '22–26 Sep. 2025', 'Unit 3 — It all went wrong', 'Past simple — regular & irregular, narrative writing'),
|
||
(4, '29 Sep–3 Oct.', 'Unit 4 — Let\'s go shopping', 'Comparative & superlative adjectives, expressing preference'),
|
||
(5, '6–10 Oct. 2025', 'Unit 5 — Plans and ambitions', 'Future forms (going to / will), goal setting'),
|
||
(6, '13–17 Oct. 2025', 'Progress test 1 (R&W + L&S)', 'Mid-term progress assessment'),
|
||
(7, '20–24 Oct. 2025', 'Unit 6 — What if…?', 'First conditional, giving advice'),
|
||
(8, '27–31 Oct. 2025', 'Unit 7 — Telling stories', 'Past continuous vs past simple'),
|
||
(9, '3–7 Nov. 2025', 'Unit 8 — Have you ever…?', 'Present perfect, life experiences'),
|
||
(10, '10–14 Nov. 2025', 'Unit 9 — How do I get there?', 'Giving directions, prepositions of place'),
|
||
(11, '17–21 Nov. 2025', 'Unit 10 — Going places', 'Travel vocabulary, modal verbs of obligation'),
|
||
(12, '24–28 Nov. 2025', 'Final exam preparation + final exam', 'Revision and final examination'),
|
||
]
|
||
week_lookup = {}
|
||
for week_number, date_label, unit, focus in WEEKS:
|
||
w = CoursePlanWeek.search([('plan_id', '=', plan.id), ('week_number', '=', week_number)], limit=1)
|
||
items = []
|
||
if week_number == 1:
|
||
items = [
|
||
{'skill': 'reading', 'outcome_codes': ['RLO1', 'RLO2', 'RLO5'], 'remarks': 'Pre-reading + 400-word text on student life.'},
|
||
{'skill': 'writing', 'outcome_codes': ['WLO1', 'WLO2'], 'remarks': 'Plan and write a personal-introduction paragraph (~150 words).'},
|
||
{'skill': 'listening', 'outcome_codes': ['LLO1', 'LLO3'], 'remarks': '3-minute monologue: a student describes her week.'},
|
||
{'skill': 'speaking', 'outcome_codes': ['SLO1', 'SLO2'], 'remarks': 'Pair work: get-to-know-you interview, 5 minutes per pair.'},
|
||
{'skill': 'grammar', 'outcome_codes': ['GLO1'], 'remarks': 'Present simple & present continuous — form, use, contrast.'},
|
||
{'skill': 'vocabulary','outcome_codes': ['VLO1'], 'remarks': 'Daily-routine verbs, free-time activities, family vocabulary.'},
|
||
]
|
||
if not w:
|
||
w = CoursePlanWeek.create({
|
||
'plan_id': plan.id,
|
||
'week_number': week_number,
|
||
'date_label': date_label,
|
||
'unit': unit,
|
||
'focus': focus,
|
||
'items_json': json.dumps(items),
|
||
})
|
||
week_lookup[week_number] = w
|
||
env.cr.commit()
|
||
print(f"✓ Course plan weeks: {len(week_lookup)} weeks present.")
|
||
|
||
# Week 1 full materials
|
||
WEEK1_MATERIALS = [
|
||
{
|
||
'skill': 'reading', 'material_type': 'reading_text',
|
||
'title': 'Reading: A Day in Maya\'s Life',
|
||
'summary': 'B1 reading passage (~390 words) about a university student\'s week, with 6 comprehension questions targeting RLO1, RLO2 and RLO5.',
|
||
'body': {
|
||
'text': (
|
||
"Maya is twenty years old and she is studying English at a college in Muscat. "
|
||
"Every weekday she wakes up at six o'clock. First, she has a small breakfast — usually eggs, "
|
||
"bread and a cup of tea — and then she takes the bus to college. The journey takes about thirty "
|
||
"minutes. While she is on the bus, she often reads or listens to a podcast. Right now she is "
|
||
"listening to an English podcast about travel because she loves visiting new places.\n\n"
|
||
"Maya's first class starts at eight. On Mondays and Wednesdays she has reading and writing "
|
||
"lessons; on Tuesdays and Thursdays she has listening and speaking. Her favourite class is "
|
||
"speaking, because she likes telling stories about her family. She does not enjoy grammar very "
|
||
"much, but she knows that grammar helps her writing, so she practises every day.\n\n"
|
||
"After her classes, Maya usually meets her friends Nora and Khalid at the cafeteria. They "
|
||
"have lunch together and they talk about their homework. In the afternoon, Maya goes to the "
|
||
"library and studies for two hours. She is preparing for the progress test next month, so she "
|
||
"is reading a lot of articles in English.\n\n"
|
||
"In the evening, Maya helps her younger brother with his English homework. Then, she has "
|
||
"dinner with her family. Before bed, she always writes in her diary in English. She thinks "
|
||
"writing every day is the best way to improve. On weekends, she does not study; instead, she "
|
||
"visits her grandparents in a small village near the coast and goes for long walks on the beach."
|
||
),
|
||
'questions': [
|
||
{'q': 'Where does Maya study English?', 'a': 'At a college in Muscat.'},
|
||
{'q': 'What does Maya usually have for breakfast?', 'a': 'Eggs, bread and a cup of tea.'},
|
||
{'q': 'Which class is Maya\'s favourite and why?', 'a': 'Speaking, because she likes telling stories about her family.'},
|
||
{'q': 'Why is Maya reading a lot of articles in English right now?', 'a': 'She is preparing for the progress test next month.'},
|
||
{'q': 'What does Maya always do before bed?', 'a': 'She writes in her diary in English.'},
|
||
{'q': 'Where does Maya go on weekends?', 'a': 'To her grandparents in a small village near the coast.'},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
'skill': 'writing', 'material_type': 'writing_prompt',
|
||
'title': 'Writing: My weekly routine (~150 words)',
|
||
'summary': 'Pre-writing → drafting → editing task targeting WLO1 and WLO2. Students plan, then write a paragraph about their own routine using present simple + at least two linking words.',
|
||
'body': {
|
||
'prompt': 'Write one paragraph (about 150 words) describing your weekly routine. Use present simple, present continuous, and at least two linking words (and, but, also, then).',
|
||
'planning_steps': [
|
||
'Brainstorm 5 things you do every week (work, study, sport, family, hobbies).',
|
||
'Group them by day or by part of the day (morning / afternoon / evening).',
|
||
'Choose one detail you want to highlight (the part you enjoy most).',
|
||
'Plan your topic sentence: "My weeks are usually …".',
|
||
'Write the paragraph in 25 minutes; then re-read it for verb forms.',
|
||
],
|
||
'rubric_summary': 'Task achievement, coherence/cohesion, lexical resource, grammatical range — each scored 0–9 (graded by writing_grader agent).',
|
||
},
|
||
},
|
||
{
|
||
'skill': 'listening', 'material_type': 'listening_script',
|
||
'title': 'Listening: My week at college (3-minute monologue)',
|
||
'summary': 'Carefully-articulated 3-minute monologue at B1 with comprehension and inference questions covering LLO1 and LLO3.',
|
||
'body': {
|
||
'script': (
|
||
"Hello, my name is Layla and I'd like to tell you about a typical week for me at college. "
|
||
"I usually start my day at half past six. I have a quick breakfast and then I go to my "
|
||
"classes. I have four classes a day, from eight in the morning until two in the afternoon. "
|
||
"On Mondays I have reading first, and that's my favourite class because the texts are always "
|
||
"interesting. On Tuesdays I have speaking, which is harder for me, but I am improving. "
|
||
"After classes I usually go to the library with my friend Hessa, and we study for about an "
|
||
"hour. In the evenings I sometimes watch a film in English, but I don't watch every night — "
|
||
"two or three times a week is enough. On Fridays my family always has lunch together. That's "
|
||
"my favourite day."
|
||
),
|
||
'comprehension_questions': [
|
||
{'q': 'What time does Layla usually start her day?', 'options': ['06:00', '06:30', '07:00', '07:30'], 'answer': '06:30'},
|
||
{'q': 'How many classes a day does Layla have?', 'options': ['2', '3', '4', '5'], 'answer': '4'},
|
||
{'q': 'Why is reading her favourite class?', 'answer': 'Because the texts are always interesting.'},
|
||
{'q': 'How often does Layla watch a film in English?', 'answer': 'Two or three times a week.'},
|
||
{'q': 'Which day is her favourite and why?', 'answer': 'Friday, because her family always has lunch together.'},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
'skill': 'speaking', 'material_type': 'speaking_prompt',
|
||
'title': 'Speaking: Get-to-know-you pair interview',
|
||
'summary': 'Pair work activity targeting SLO1 and SLO2. Students prepare 5 questions, conduct a 5-minute interview, then report 3 facts about their partner to the class.',
|
||
'body': {
|
||
'instructions': 'In pairs, ask and answer the questions below. Take notes. Then change partner and report 3 things you learned.',
|
||
'questions': [
|
||
'Where are you from and how long have you lived there?',
|
||
'What do you usually do at the weekend?',
|
||
'What are you doing this term that you didn\'t do last term?',
|
||
'Tell me about one person in your family who inspires you. Why?',
|
||
'What is one thing you would like to do better in English by the end of this course?',
|
||
],
|
||
'success_criteria': [
|
||
'Use present simple for habits and present continuous for current activities.',
|
||
'Use at least 3 linking words (and, but, because, also, then).',
|
||
'When you don\'t understand, ask for repetition: "Sorry, can you say that again?"',
|
||
],
|
||
'duration_minutes': 5,
|
||
},
|
||
},
|
||
{
|
||
'skill': 'grammar', 'material_type': 'grammar_lesson',
|
||
'title': 'Grammar: Present simple vs present continuous',
|
||
'summary': 'Mini-lesson, contrastive examples, and 8 controlled-practice items. Targets GLO1.',
|
||
'body': {
|
||
'explanation': (
|
||
"Use the **present simple** for routines, facts, and things that are generally true. "
|
||
"Use the **present continuous** for actions happening now or around now, and for "
|
||
"temporary situations.\n\n"
|
||
"Time expressions: every day, on Mondays, twice a week, usually, often → present simple.\n"
|
||
"Time expressions: now, at the moment, today, this week → present continuous."
|
||
),
|
||
'examples': [
|
||
'Maya **has** breakfast at 6 o\'clock every day. (routine — present simple)',
|
||
'Right now, she **is listening** to a podcast on the bus. (now — present continuous)',
|
||
'I **don\'t usually study** at night, but this week I **am studying** late. (contrast)',
|
||
],
|
||
'practice': [
|
||
{'q': 'Right now I ____ (read) a great book.', 'a': 'am reading'},
|
||
{'q': 'My brother ____ (work) at a hospital every weekend.', 'a': 'works'},
|
||
{'q': 'Look! It ____ (rain) again.', 'a': 'is raining'},
|
||
{'q': 'Water ____ (boil) at 100°C.', 'a': 'boils'},
|
||
{'q': 'They ____ (not / live) here this month.', 'a': 'are not living'},
|
||
{'q': 'How often ____ you ____ (go) to the cinema?', 'a': 'do … go'},
|
||
{'q': 'I ____ (study) hard for the test these days.', 'a': 'am studying'},
|
||
{'q': 'My mother always ____ (cook) on Fridays.', 'a': 'cooks'},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
'skill': 'vocabulary', 'material_type': 'vocabulary_list',
|
||
'title': 'Vocabulary: Daily routines & family',
|
||
'summary': '24 high-frequency B1 items grouped by topic with example sentences. Targets VLO1.',
|
||
'body': {
|
||
'groups': [
|
||
{'topic': 'Daily routines', 'items': [
|
||
{'word': 'wake up', 'example': 'I wake up at six every weekday.'},
|
||
{'word': 'have breakfast', 'example': 'We always have breakfast together.'},
|
||
{'word': 'commute', 'example': 'I commute to college by bus.'},
|
||
{'word': 'attend class', 'example': 'She attends class every Monday.'},
|
||
{'word': 'do homework', 'example': 'I do my homework after dinner.'},
|
||
{'word': 'go to bed', 'example': 'I go to bed at eleven.'},
|
||
]},
|
||
{'topic': 'Family', 'items': [
|
||
{'word': 'parents', 'example': 'My parents live in Muscat.'},
|
||
{'word': 'siblings', 'example': 'I have two siblings.'},
|
||
{'word': 'grandparents', 'example': 'My grandparents are very kind.'},
|
||
{'word': 'cousin', 'example': 'My cousin is studying medicine.'},
|
||
{'word': 'relatives', 'example': 'We visit our relatives at Eid.'},
|
||
]},
|
||
{'topic': 'Free-time', 'items': [
|
||
{'word': 'go for a walk', 'example': 'On Fridays I go for a walk on the beach.'},
|
||
{'word': 'watch a series', 'example': 'I watch a series in English every night.'},
|
||
{'word': 'read a novel', 'example': 'She is reading a novel in English.'},
|
||
{'word': 'listen to a podcast', 'example': 'He listens to a podcast on the bus.'},
|
||
{'word': 'play a sport', 'example': 'They play a sport twice a week.'},
|
||
]},
|
||
],
|
||
},
|
||
},
|
||
]
|
||
|
||
mat_created = 0
|
||
for mat in WEEK1_MATERIALS:
|
||
existing = CoursePlanMat.search([
|
||
('plan_id', '=', plan.id),
|
||
('week_id', '=', week_lookup[1].id),
|
||
('title', '=', mat['title']),
|
||
], limit=1)
|
||
if existing:
|
||
continue
|
||
CoursePlanMat.create({
|
||
'plan_id': plan.id,
|
||
'week_id': week_lookup[1].id,
|
||
'skill': mat['skill'],
|
||
'material_type': mat['material_type'],
|
||
'title': mat['title'],
|
||
'summary': mat['summary'],
|
||
'body_json': json.dumps(mat['body']),
|
||
'body_text': mat['summary'],
|
||
})
|
||
mat_created += 1
|
||
env.cr.commit()
|
||
print(f"✓ Week 1 materials: existing kept, newly created={mat_created}")
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# 4. Sample writing + speaking submissions tied to AI grader output
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
AiLog = env['encoach.ai.log']
|
||
AiFeedback = env['encoach.ai.feedback']
|
||
|
||
# Lightweight grader log entries — using the real schema:
|
||
# service ∈ openai/whisper/polly/elevenlabs/gptzero/elai/coach
|
||
# action is the free-text dispatch label, here we put the agent key.
|
||
sample_runs = [
|
||
{
|
||
'service': 'openai', 'action': 'agent:writing_grader', 'model_used': 'gpt-4o',
|
||
'user_id': khalid.id,
|
||
'prompt_tokens': 180, 'completion_tokens': 130, 'total_tokens': 310,
|
||
'latency_ms': 5300, 'status': 'success',
|
||
'input_preview': f'Grade writing for {sarah.name}: "Last weekend I visited Sharjah Aquarium…"',
|
||
'output_preview': json.dumps({
|
||
'overall_band': 6,
|
||
'scores': {'task_achievement': 7, 'coherence_cohesion': 6, 'lexical_resource': 6, 'grammatical_range': 5},
|
||
'feedback': 'Good task achievement. Watch verb forms ("we taked" → "we took"). Add more linking words.',
|
||
}),
|
||
},
|
||
{
|
||
'service': 'openai', 'action': 'agent:speaking_grader', 'model_used': 'gpt-4o',
|
||
'user_id': fatima.id,
|
||
'prompt_tokens': 220, 'completion_tokens': 160, 'total_tokens': 380,
|
||
'latency_ms': 6700, 'status': 'success',
|
||
'input_preview': f'Grade speaking for {omar.name}: 90-second monologue, transcript attached.',
|
||
'output_preview': json.dumps({
|
||
'overall_band': 6,
|
||
'scores': {'fluency_coherence': 6, 'lexical_resource': 6, 'grammatical_range': 6, 'pronunciation': 5},
|
||
'feedback': 'Maintains clear speech with some hesitation. Self-correction is good.',
|
||
}),
|
||
},
|
||
{
|
||
'service': 'openai', 'action': 'agent:lms_tutor', 'model_used': 'gpt-4o-mini',
|
||
'user_id': sarah.id,
|
||
'prompt_tokens': 95, 'completion_tokens': 240, 'total_tokens': 335,
|
||
'latency_ms': 13000, 'status': 'success',
|
||
'input_preview': 'Tutor: present continuous tense — give an example and a B1 tip.',
|
||
'output_preview': '"I am studying English right now." Tip: focus on coherence/cohesion in writing. Tools called: resources.search, outcomes.fetch.',
|
||
},
|
||
]
|
||
runs_created = 0
|
||
for run in sample_runs:
|
||
if AiLog.search([('service', '=', run['service']), ('action', '=', run['action']), ('user_id', '=', run['user_id'])], limit=1):
|
||
continue
|
||
AiLog.create(run)
|
||
runs_created += 1
|
||
|
||
# Feedback rows so the prompts page shows activity.
|
||
# subject_type ∈ question/coach/explanation/translation/narrative/other; rating ∈ up/down.
|
||
existing_feedback = AiFeedback.search_count([('subject_type', '=', 'other')])
|
||
fb_specs = [
|
||
{'subject_id': 1, 'rating': 'up', 'user_id': khalid.id, 'comment': 'writing_grader: feedback was specific and actionable.'},
|
||
{'subject_id': 2, 'rating': 'up', 'user_id': fatima.id, 'comment': 'speaking_grader: scores aligned with my own assessment.'},
|
||
{'subject_id': 3, 'rating': 'down', 'user_id': admin.id, 'comment': 'lms_tutor: answer was good, but tool retrieval returned a noisy resource.'},
|
||
]
|
||
fb_created = 0
|
||
for fb in fb_specs:
|
||
dup = AiFeedback.search([
|
||
('subject_type', '=', 'other'),
|
||
('subject_id', '=', fb['subject_id']),
|
||
('user_id', '=', fb['user_id']),
|
||
], limit=1)
|
||
if dup:
|
||
continue
|
||
AiFeedback.create({
|
||
'subject_type': 'other',
|
||
'subject_id': fb['subject_id'],
|
||
'rating': fb['rating'],
|
||
'user_id': fb['user_id'],
|
||
'comment': fb['comment'],
|
||
'entity_id': entity.id,
|
||
})
|
||
fb_created += 1
|
||
env.cr.commit()
|
||
print(f"✓ Agent runs added: {runs_created}; AI feedback rows added: {fb_created}")
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
# Summary
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
print("\n" + "─" * 72)
|
||
print(" Demo seed complete. Quick credentials reference:")
|
||
print("─" * 72)
|
||
for u in DEMO_USERS:
|
||
print(f" {u['user_type']:<16} {u['login']:<32} {u['password']}")
|
||
print("─" * 72)
|
||
print(f" Approval workflow: {wf.name} (id={wf.id}) — assigned approver={approver.login}")
|
||
print(f" Course plan: {plan.name} (id={plan.id}) — {plan.total_weeks} weeks, "
|
||
f"{len(plan.material_ids)} materials")
|
||
print("─" * 72 + "\n")
|