feat: EnCoach V2 — complete OWL refactor with 15 new modules
Full architectural refactor from React to Odoo OWL: - 15 new Odoo modules: signup, placement, exam_template, scoring, course_gen, entity_onboard, ai_course, quality_gate, ielts_validation, pdf_report, verification, math, it, portal, exam_session - 6 modified modules: core (new user fields), exam (template types), adaptive (events/paths/settings), branding (white-label), resources (CEFR/AI fields), taxonomy (Math+IT hierarchies) - ~79 new REST API endpoints across all controller packages - ~50 admin backend views (form/list/kanban/search/menu) - 5 custom OWL components: dashboard, content pool, exam validation, gap analysis, adaptive timeline - POS-inspired full-screen exam session with 9 question renderers - Student portal with 12 website pages (QWeb + OWL) - AI/ML services: IRT 3PL CAT engine, CEFR mapper, quality gates, IELTS validator, SymPy math scorer, speaking evaluator, adaptive engine - Seed data: IELTS/TOEFL/STEP/IC3 templates, 30+ sample questions, English/Math/IT taxonomy trees with 50+ topics - Updated requirements.txt with new dependencies Made-with: Cursor
This commit is contained in:
1
new_project/custom_addons/encoach_math/__init__.py
Normal file
1
new_project/custom_addons/encoach_math/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import services
|
||||
18
new_project/custom_addons/encoach_math/__manifest__.py
Normal file
18
new_project/custom_addons/encoach_math/__manifest__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
'name': 'EnCoach Math',
|
||||
'version': '19.0.1.0',
|
||||
'category': 'Education',
|
||||
'summary': 'Mathematical question types: numerical, expression, matrix scoring via SymPy',
|
||||
'author': 'EnCoach',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['encoach_core', 'encoach_exam_template'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['sympy'],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
@@ -0,0 +1 @@
|
||||
from .math_scorer import MathScorer
|
||||
113
new_project/custom_addons/encoach_math/services/math_scorer.py
Normal file
113
new_project/custom_addons/encoach_math/services/math_scorer.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MathScorer:
|
||||
"""Score mathematical question types using SymPy."""
|
||||
|
||||
@staticmethod
|
||||
def score_numerical(student_answer, correct_answer):
|
||||
"""Score a numerical answer with tolerance.
|
||||
correct_answer: {"value": float, "tolerance": float}
|
||||
"""
|
||||
try:
|
||||
student_val = float(student_answer)
|
||||
correct_val = float(correct_answer.get('value', 0))
|
||||
tolerance = float(correct_answer.get('tolerance', 0.01))
|
||||
|
||||
correct = abs(student_val - correct_val) <= tolerance
|
||||
return {
|
||||
'correct': correct,
|
||||
'score': 1.0 if correct else 0.0,
|
||||
'feedback': 'Correct!' if correct else f'Expected {correct_val} (±{tolerance}), got {student_val}',
|
||||
}
|
||||
except (ValueError, TypeError) as e:
|
||||
return {'correct': False, 'score': 0.0, 'feedback': f'Invalid numerical input: {e}'}
|
||||
|
||||
@staticmethod
|
||||
def score_expression(student_expr, correct_expr):
|
||||
"""Score mathematical expressions using symbolic equivalence."""
|
||||
try:
|
||||
import sympy
|
||||
student_sym = sympy.sympify(student_expr, evaluate=True)
|
||||
correct_sym = sympy.sympify(correct_expr, evaluate=True)
|
||||
|
||||
diff = sympy.simplify(student_sym - correct_sym)
|
||||
correct = diff == 0 or sympy.simplify(diff).is_zero
|
||||
|
||||
if not correct:
|
||||
try:
|
||||
student_float = float(student_sym.evalf())
|
||||
correct_float = float(correct_sym.evalf())
|
||||
correct = abs(student_float - correct_float) < 1e-6
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
'correct': correct,
|
||||
'score': 1.0 if correct else 0.0,
|
||||
'feedback': 'Correct!' if correct else f'Your answer {student_expr} is not equivalent to {correct_expr}',
|
||||
'simplified_student': str(sympy.simplify(student_sym)),
|
||||
'simplified_correct': str(sympy.simplify(correct_sym)),
|
||||
}
|
||||
except ImportError:
|
||||
return {'correct': False, 'score': 0.0, 'feedback': 'SymPy not available for expression scoring'}
|
||||
except Exception as e:
|
||||
return {'correct': False, 'score': 0.0, 'feedback': f'Error parsing expression: {e}'}
|
||||
|
||||
@staticmethod
|
||||
def score_matrix(student_matrix, correct_matrix, tolerance=0.01):
|
||||
"""Score matrix answers with element-wise comparison."""
|
||||
try:
|
||||
import sympy
|
||||
|
||||
if isinstance(student_matrix, str):
|
||||
import json
|
||||
student_matrix = json.loads(student_matrix)
|
||||
if isinstance(correct_matrix, str):
|
||||
import json
|
||||
correct_matrix = json.loads(correct_matrix)
|
||||
|
||||
s_mat = sympy.Matrix(student_matrix)
|
||||
c_mat = sympy.Matrix(correct_matrix)
|
||||
|
||||
if s_mat.shape != c_mat.shape:
|
||||
return {
|
||||
'correct': False, 'score': 0.0,
|
||||
'feedback': f'Matrix dimensions mismatch: expected {c_mat.shape}, got {s_mat.shape}',
|
||||
}
|
||||
|
||||
total_elements = s_mat.rows * s_mat.cols
|
||||
correct_elements = 0
|
||||
for i in range(s_mat.rows):
|
||||
for j in range(s_mat.cols):
|
||||
if abs(float(s_mat[i, j]) - float(c_mat[i, j])) <= tolerance:
|
||||
correct_elements += 1
|
||||
|
||||
all_correct = correct_elements == total_elements
|
||||
partial_score = correct_elements / total_elements if total_elements > 0 else 0
|
||||
|
||||
return {
|
||||
'correct': all_correct,
|
||||
'score': 1.0 if all_correct else partial_score,
|
||||
'feedback': 'Correct!' if all_correct else f'{correct_elements}/{total_elements} elements correct',
|
||||
'correct_elements': correct_elements,
|
||||
'total_elements': total_elements,
|
||||
}
|
||||
except ImportError:
|
||||
return {'correct': False, 'score': 0.0, 'feedback': 'SymPy not available for matrix scoring'}
|
||||
except Exception as e:
|
||||
return {'correct': False, 'score': 0.0, 'feedback': f'Error scoring matrix: {e}'}
|
||||
|
||||
@staticmethod
|
||||
def evaluate_expression(expr_str):
|
||||
"""Safely evaluate a mathematical expression."""
|
||||
try:
|
||||
import sympy
|
||||
result = sympy.sympify(expr_str, evaluate=True)
|
||||
return float(result.evalf())
|
||||
except ImportError:
|
||||
raise ValueError("SymPy not available")
|
||||
except Exception as e:
|
||||
raise ValueError(f"Cannot evaluate expression: {e}")
|
||||
Reference in New Issue
Block a user