Files
full_encoach_platform/test_ai_full.py
Yamen Ahmad 98b9837a54 feat: institutional + support + training admin sections (backend + frontend)
Ship three fully-wired admin areas end-to-end with APIs, seeds, tests and docs.

Backend (new `encoach_lms_api` addon + existing addons):
- Institutional: academic years/terms, departments, admission registers & admissions,
  courses/batches, lessons, fees (terms + student fees + invoicing with income-account
  auto-wiring), gradebook (assignments/grades), library, facilities (encoach.asset),
  student leave, result templates + marksheets (incl. delete-with-cascade).
- Support: `encoach.ticket` model + CRUD/assignee routes; payment records derived
  from `op.student.fees.details` and `account.move`; platform settings backed by
  `encoach.code` and `ir.config_parameter` (packages + grading config).
- Training: `encoach.vocab.item` + `encoach.grammar.rule` (plus progress models)
  with CRUD, pagination, search/level filters, and upsert-style progress endpoints.
  Odoo 19 compatibility: `_sql_constraints` replaced with `@api.constrains`;
  `ValidationError`/`UserError` mapped to HTTP 400.

Frontend:
- Rewire institutional admin pages (Academic Year Manager, Admissions, Courses,
  Lessons, Fees, Gradebook, Library, Facilities, Student Leave, Marksheets,
  Taxonomy, Resources) to real APIs with React Query invalidation and dialogs.
- New typed services: `payments.service.ts`, `platformSettings.service.ts`,
  `training.service.ts`. Updated `fees/gradebook/lms/courseware/taxonomy/
  resources/student-progress/generation` services + related types.
- Rewrite `VocabularyPage`, `GrammarPage`, `PaymentRecordPage`, `SettingsPage`,
  `TicketsPage` to consume live data with search/filter/progress/CRUD flows.
- New shared components: `TaxonomyCascade`, `MaterialViewer`, `teacher/TeacherLibrary`.
- Favicons/branding assets and misc. UX polish across teacher/student pages.

Tooling & QA:
- Seeders: `seed_demo.py`, `seed_demo_data.py`, `seed_institutional.py` (idempotent,
  covers institutional + support + training fixtures incl. income-account wiring).
- API write-flow test suites: `test_write_flows.py` (institutional),
  `test_support_flows.py` (support), `test_training_flows.py` (training),
  `test_ai_full.py`. All suites pass end-to-end.
- Docs: add `docs/PROJECT_SUMMARY.md` with per-section scope, artifacts and QA.
- `.gitignore`: ignore `pgdata_bak_*/`, `frontend/.vite/`, `frontend/dist/`,
  `frontend/node_modules/`.

Made-with: Cursor
2026-04-19 03:13:23 +04:00

425 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Full AI + Vector Database test scenario using real API keys.
Run in Odoo shell:
python3 odoo/odoo-bin shell -c odoo.conf -d encoach_v2 --no-http < test_ai_full.py
"""
import json, time, traceback
ICP = env['ir.config_parameter'].sudo()
results = []
def test(name, func):
print(f"\n{'='*60}")
print(f"TEST: {name}")
print(f"{'='*60}")
start = time.time()
try:
result = func()
elapsed = round((time.time() - start) * 1000)
results.append(('PASS', name, elapsed, None))
print(f" ✓ PASS ({elapsed}ms)")
return result
except Exception as e:
elapsed = round((time.time() - start) * 1000)
results.append(('FAIL', name, elapsed, str(e)))
print(f" ✗ FAIL ({elapsed}ms): {e}")
traceback.print_exc()
return None
# ══════════════════════════════════════════════════════════════
# PHASE 1: Configuration verification
# ══════════════════════════════════════════════════════════════
def test_config():
key = ICP.get_param('encoach_ai.openai_api_key', '')
assert key.startswith('sk-'), f"OpenAI key missing or invalid: {key[:10]}..."
assert ICP.get_param('encoach_ai.enabled') == 'True' or ICP.get_param('encoach_ai.ai_enabled') == 'True', "AI not enabled"
model = ICP.get_param('encoach_ai.openai_model', '')
assert model == 'gpt-4o', f"Model is '{model}', expected 'gpt-4o'"
jwt = ICP.get_param('encoach.jwt_secret', '')
assert len(jwt) > 10, "JWT secret missing"
print(f" OpenAI key: {key[:15]}...")
print(f" Model: {model}")
print(f" AI enabled: True")
return True
test('1. AI Configuration', test_config)
# ══════════════════════════════════════════════════════════════
# PHASE 2: OpenAI direct connectivity
# ══════════════════════════════════════════════════════════════
def test_openai_direct():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Say hello in exactly 5 words."}
]
response = svc.chat(messages)
assert response and len(response) > 0, "Empty response from OpenAI"
print(f" OpenAI response: {response[:100]}")
return response
test('2. OpenAI Direct Chat', test_openai_direct)
def test_openai_json():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
messages = [
{"role": "system", "content": "You are a JSON generator. Return only valid JSON."},
{"role": "user", "content": "Return a JSON object with keys: greeting (string), count (number), items (array of 3 strings)."}
]
response = svc.chat_json(messages)
assert isinstance(response, dict), f"Expected dict, got {type(response)}"
assert 'greeting' in response or 'items' in response, f"Unexpected keys: {list(response.keys())}"
print(f" JSON response: {json.dumps(response, indent=2)[:200]}")
return response
test('3. OpenAI JSON Mode', test_openai_json)
# ══════════════════════════════════════════════════════════════
# PHASE 3: Vector Database tests
# ══════════════════════════════════════════════════════════════
def test_vector_count():
Embedding = env['encoach.embedding'].sudo()
total = Embedding.search_count([])
env.cr.execute("SELECT count(*) FROM encoach_embedding WHERE embedding IS NOT NULL")
with_vec = env.cr.fetchone()[0]
assert total > 0, "No embeddings found"
assert with_vec > 0, "No records have vector data"
print(f" Total embeddings: {total}")
print(f" With vectors: {with_vec}")
return total
test('4. Vector DB — Record Count', test_vector_count)
def test_vector_search_resources():
from odoo.addons.encoach_vector.services.embedding_service import EmbeddingService
svc = EmbeddingService(env)
results = svc.search("academic writing improvement strategies", content_type='resource', limit=5)
assert len(results) > 0, "No search results"
print(f" Found {len(results)} results:")
for r in results:
print(f" [{r['similarity']:.4f}] {r['content_type']}: {r['text'][:60]}")
assert results[0]['similarity'] > 0.2, f"Top similarity too low: {results[0]['similarity']}"
return results
test('5. Vector Search — Resources', test_vector_search_resources)
def test_vector_search_modules():
from odoo.addons.encoach_vector.services.embedding_service import EmbeddingService
svc = EmbeddingService(env)
results = svc.search("speaking fluency pronunciation practice", content_type='module', limit=3)
print(f" Found {len(results)} module results:")
for r in results:
print(f" [{r['similarity']:.4f}] {r['text'][:60]}")
return results
test('6. Vector Search — Modules', test_vector_search_modules)
def test_vector_search_cross_type():
from odoo.addons.encoach_vector.services.embedding_service import EmbeddingService
svc = EmbeddingService(env)
results = svc.search("IELTS preparation course material", limit=10)
types_found = set(r['content_type'] for r in results)
print(f" Found {len(results)} results across types: {types_found}")
for r in results[:5]:
print(f" [{r['similarity']:.4f}] {r['content_type']}: {r['text'][:60]}")
return results
test('7. Vector Search — Cross-Type', test_vector_search_cross_type)
# ══════════════════════════════════════════════════════════════
# PHASE 4: AI Coach tests (correct signatures)
# ══════════════════════════════════════════════════════════════
def test_coach_chat():
from odoo.addons.encoach_ai.services.coach_service import CoachService
coach = CoachService(env)
response = coach.chat(
"I'm preparing for IELTS and struggling with the writing section. Can you help me?",
history=[],
student_context={'student_name': 'Sarah', 'cefr_level': 'B2', 'target_band': 7.0}
)
assert response and isinstance(response, dict), "Empty coach response"
reply = response.get('reply', '')
assert len(reply) > 10, f"Reply too short: {reply}"
print(f" Coach reply: {reply[:200]}...")
return response
test('8. AI Coach — Chat', test_coach_chat)
def test_coach_tip():
from odoo.addons.encoach_ai.services.coach_service import CoachService
coach = CoachService(env)
tip = coach.get_tip(context='writing')
assert tip and isinstance(tip, dict), "Empty tip"
print(f" Study tip: {json.dumps(tip)[:200]}...")
return tip
test('9. AI Coach — Study Tip', test_coach_tip)
def test_coach_explain():
from odoo.addons.encoach_ai.services.coach_service import CoachService
coach = CoachService(env)
explanation = coach.explain(
score_data={'writing': 5.5, 'reading': 6.5, 'speaking': 6.0, 'listening': 6.0, 'overall': 6.0},
student_context='Sarah is a B2 student targeting band 7.0'
)
assert explanation, "Empty explanation"
exp_text = explanation if isinstance(explanation, str) else json.dumps(explanation)
print(f" Score explanation: {exp_text[:250]}...")
return explanation
test('10. AI Coach — Score Explanation', test_coach_explain)
def test_coach_suggest():
from odoo.addons.encoach_ai.services.coach_service import CoachService
coach = CoachService(env)
suggestions = coach.suggest({
'skill': 'writing',
'current_band': 5.5,
'target_band': 7.0,
'weaknesses': ['coherence', 'task achievement'],
})
assert suggestions, "Empty suggestions"
sug_text = suggestions if isinstance(suggestions, str) else json.dumps(suggestions)
print(f" Suggestions: {sug_text[:250]}...")
return suggestions
test('11. AI Coach — Study Suggestions', test_coach_suggest)
def test_coach_writing_help():
from odoo.addons.encoach_ai.services.coach_service import CoachService
coach = CoachService(env)
help_result = coach.writing_help(
task="Some people believe that technology has made our lives more complex rather than simpler. Discuss.",
draft="In the modern era, technology has become an integral part of our daily lifes. While some people argue that it has simplified our routines, others believe it has introduced unnecessary complexities. In my opinion, technology has both simplified and complicated our lives in different way.",
help_type="review"
)
assert help_result, "Empty writing help"
help_text = help_result if isinstance(help_result, str) else json.dumps(help_result)
print(f" Writing help: {help_text[:300]}...")
return help_result
test('12. AI Coach — Writing Help', test_coach_writing_help)
def test_coach_hint():
from odoo.addons.encoach_ai.services.coach_service import CoachService
coach = CoachService(env)
hint = coach.get_hint({
'question': 'What did Fleming discover in 1928?',
'type': 'mcq',
'options': ['Penicillin', 'Aspirin', 'Insulin', 'Morphine'],
})
assert hint and isinstance(hint, dict), "Empty hint"
print(f" Hint: {json.dumps(hint)[:200]}...")
return hint
test('13. AI Coach — Hint', test_coach_hint)
# ══════════════════════════════════════════════════════════════
# PHASE 5: AI Content Generation (correct signatures)
# ══════════════════════════════════════════════════════════════
def test_generate_insights():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
insights = svc.generate_insights(
data_summary={
'total_students': 3,
'avg_band': 6.1,
'skill_breakdown': {'writing': 5.7, 'reading': 6.3, 'speaking': 6.2, 'listening': 6.2},
'trend': 'improving',
},
insight_type='dashboard'
)
assert insights, "Empty insights"
print(f" Insights: {json.dumps(insights)[:250]}...")
return insights
test('14. AI Insights Generation', test_generate_insights)
def test_generate_resource():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
resource = svc.generate_content(
content_type='reading_passage',
brief='Create a short IELTS academic reading passage about renewable energy. Include 3 true/false/not given questions.',
cefr_level='B2'
)
assert resource, "Empty resource"
res_text = resource if isinstance(resource, str) else json.dumps(resource)
print(f" Generated content: {res_text[:300]}...")
return resource
test('15. AI Content Generation', test_generate_resource)
def test_ai_grade_writing():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
grade = svc.grade_writing(
rubric="IELTS Academic Writing Task 2: Task Achievement, Coherence & Cohesion, Lexical Resource, Grammatical Range & Accuracy. Band 9: Expert user. Band 7: Good user. Band 5: Modest user.",
task_text="Some people believe that technology has made our lives more complex rather than simpler. To what extent do you agree or disagree?",
response_text="In the modern era, technology has become an integral part of our daily lifes. While some people argue that it has simplified our routines, others believe it has introduced unnecessary complexities. In my opinion, technology has both simplified and complicated our lives in different ways. On one hand, technology has made communication faster and more efficient. Social media platforms and instant messaging applications allow people to connect with others across the globe in real time. Furthermore, the internet has provided access to vast amounts of information, making education and research more accessible than ever before. On the other hand, the constant connectivity that technology provides can lead to information overload and digital fatigue. Many people find it difficult to disconnect from their devices, which can negatively impact mental health and personal relationships. Additionally, the rapid pace of technological change can be overwhelming for those who struggle to keep up with new developments."
)
assert grade, "Empty grade"
print(f" AI Grade: {json.dumps(grade)[:300]}...")
return grade
test('16. AI Writing Grading', test_ai_grade_writing)
# ══════════════════════════════════════════════════════════════
# PHASE 6: RAG (Vector-enhanced AI search)
# ══════════════════════════════════════════════════════════════
def test_rag_search():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
result = svc.search_with_rag(
query="What resources are available for improving IELTS writing skills?",
context="Student is B2 level targeting band 7.0"
)
assert result, "Empty RAG result"
result_text = result if isinstance(result, str) else json.dumps(result)
print(f" RAG Search result: {result_text[:300]}...")
return result
test('17. RAG Search (Vector + AI)', test_rag_search)
def test_rag_explain():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
messages = [
{"role": "system", "content": "You are an IELTS preparation expert. Explain in detail."},
{"role": "user", "content": "How can I improve my IELTS listening score from 6.0 to 7.0?"}
]
result = svc.chat_with_context(
messages, "IELTS listening improvement",
content_types=["resource", "module"],
model=svc.fast_model
)
assert result and len(result) > 20, "Empty RAG explain"
print(f" RAG Explain: {result[:250]}...")
return result
test('18. RAG Contextual Explanation', test_rag_explain)
# ══════════════════════════════════════════════════════════════
# PHASE 7: AI Workbench & Exam Generation
# ══════════════════════════════════════════════════════════════
def test_workbench_outline():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
messages = [
{"role": "system", "content": "You are an IELTS course designer. Return only valid JSON."},
{"role": "user", "content": "Generate a course outline for an IELTS Writing preparation course. Return JSON with: title, description, chapters (array of objects with title, description, skills array)."}
]
outline = svc.chat_json(messages)
assert isinstance(outline, dict), f"Expected dict, got {type(outline)}"
print(f" Outline: {json.dumps(outline, indent=2)[:300]}...")
return outline
test('19. AI Workbench — Course Outline', test_workbench_outline)
def test_exam_generation():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
messages = [
{"role": "system", "content": "You are an IELTS exam question writer. Return only valid JSON."},
{"role": "user", "content": "Generate 3 IELTS-style reading comprehension questions about climate change. Return JSON with: questions (array), each having: stem (string), type (mcq/tfng/gap_fill), options (array for mcq, null otherwise), correct_answer (string), difficulty (easy/medium/hard)."}
]
exam = svc.chat_json(messages)
assert isinstance(exam, dict), f"Expected dict, got {type(exam)}"
qs = exam.get('questions', [])
print(f" Generated {len(qs)} questions")
for q in qs[:3]:
print(f" [{q.get('difficulty','?')}] {q.get('type','?')}: {q.get('stem','')[:60]}")
return exam
test('20. AI Exam Question Generation', test_exam_generation)
def test_learning_plan():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
plan = svc.suggest_study_plan({
'skill': 'writing',
'current_band': 5.5,
'target_band': 7.0,
'weaknesses': ['coherence', 'task achievement'],
'available_hours_per_week': 10,
})
assert isinstance(plan, dict), f"Expected dict, got {type(plan)}"
print(f" Study plan: {json.dumps(plan)[:250]}...")
return plan
test('21. AI Learning Plan Generation', test_learning_plan)
def test_report_narrative():
from odoo.addons.encoach_ai.services.openai_service import OpenAIService
svc = OpenAIService(env)
narrative = svc.generate_report_narrative(
report_type='class_progress',
data={
'class_name': 'IELTS 2026 Spring',
'students': 3,
'avg_band': 6.1,
'trend': 'improving',
'top_weaknesses': ['writing coherence', 'speaking fluency'],
'top_strengths': ['reading comprehension', 'vocabulary'],
}
)
assert narrative and len(narrative) > 20, "Empty narrative"
print(f" Narrative: {narrative[:300]}...")
return narrative
test('22. AI Report Narrative', test_report_narrative)
# ══════════════════════════════════════════════════════════════
# FINAL REPORT
# ══════════════════════════════════════════════════════════════
print("\n\n" + "="*70)
print(" FULL AI + VECTOR DB TEST REPORT")
print("="*70)
passed = sum(1 for r in results if r[0] == 'PASS')
failed = sum(1 for r in results if r[0] == 'FAIL')
total_time = sum(r[2] for r in results)
for status, name, elapsed, err in results:
icon = "" if status == 'PASS' else ""
err_msg = f"{err[:80]}" if err else ""
print(f" {icon} {status} ({elapsed:>5}ms) {name}{err_msg}")
print(f"\n TOTAL: {passed} passed, {failed} failed out of {len(results)} tests")
print(f" Total time: {total_time}ms ({total_time/1000:.1f}s)")
print("="*70)