feat(backend): Phase 2/3 hardening release
Roadmap P0 — platform safety & ops
- Merge duplicate encoach.student.attempt/answer models into encoach_scoring
and drop the stale encoach_exam_template copies.
- Remove duplicate /api/exam/* routes; canonicalize on one controller tree.
- Gate raw-SQL seeds in seed_demo_data.py behind an explicit env flag.
- Add /api/health and /api/health/ready (DB + LLM reachability) endpoints.
- Fix docker-compose + ship odoo-docker.conf for container-local runs.
- Enforce OpenAI request_timeout=30s and @jwt_required on all AI/coach routes.
- Promote canonical cefr_mapper to encoach_ai.services.cefr_mapper.
- JWT cache TTL=30s + invalidation hook on user mutation.
Roadmap P1 — exam correctness & data provenance
- Wire QualityChecker + IeltsValidator into exam submit with a
pending_review gate (encoach_ai.services.question_validator).
- Populate RAG metadata (course_id, subject_id, entity_id, taxonomy) on
encoach_vector embeddings and add a chunking pipeline (>2000 chars).
- Add provenance fields on encoach.question (model, prompt_hash, log_id)
and validate LLM output with schema before DB insert.
- Unify response envelope to {items,total,page,size}.
- Approval reject rollback with savepoint atomicity.
- Ticket notifications on status/assignee change.
Roadmap P2 — performance & observability
- Reports: replace Python loops with SQL read_group aggregations.
- X-Request-ID middleware + structured JSON logs.
- In-process/Prometheus counters and openapi.py controller exporting a
spec by scanning @http.route decorators.
- Paymob real checkout + HMAC-SHA512 webhook verification, backed by a
new encoach.paymob.order model and ir.config_parameter credentials.
- JWT refresh tokens + revocation table.
- Composite DB indexes on hot report/ticket/attempt paths.
Roadmap P3 — human-in-the-loop & compliance
- Human-in-the-loop exam review workflow (pending_review → publish) with
new review controller and status transitions.
- encoach.ai.prompt model + versioning + admin editor endpoints (one
active version per key, render-preview dry run).
- Student feedback loop → encoach.ai.feedback (upsert per user/subject,
admin triage + resolve endpoints).
- GDPR export (/api/gdpr/export) and right-to-erasure (/api/gdpr/delete)
with anonymization, tombstone record, and admin-self-erasure guard.
- HttpCase smoke tests for /api/health and /api/health/ready.
Made-with: Cursor
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
'summary': 'Computerized Adaptive Testing (CAT) placement engine with CEFR mapping',
|
||||
'author': 'EnCoach',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['encoach_core', 'encoach_exam_template'],
|
||||
'depends': ['encoach_core', 'encoach_exam_template', 'encoach_ai'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/cat_session_views.xml',
|
||||
|
||||
@@ -8,6 +8,7 @@ from odoo.http import request
|
||||
from odoo.addons.encoach_api.controllers.base import (
|
||||
jwt_required, _json_response, _error_response, _get_json_body, _paginate
|
||||
)
|
||||
from odoo.addons.encoach_ai.services.cefr_mapper import theta_to_cefr as _theta_to_cefr
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,18 +16,6 @@ LEARNING_RATE = 0.3
|
||||
SEM_THRESHOLD = 0.3
|
||||
MAX_QUESTIONS = 40
|
||||
|
||||
THETA_CEFR = [
|
||||
(-3.0, 'pre_a1'), (-2.0, 'a1'), (-1.0, 'a2'),
|
||||
(0.0, 'b1'), (1.0, 'b2'), (2.0, 'c1'), (3.0, 'c2'),
|
||||
]
|
||||
|
||||
|
||||
def _theta_to_cefr(theta):
|
||||
for boundary, level in THETA_CEFR:
|
||||
if theta <= boundary:
|
||||
return level
|
||||
return 'c2'
|
||||
|
||||
|
||||
def _irt_probability(theta, a, b, c):
|
||||
"""3PL IRT probability."""
|
||||
|
||||
@@ -1,83 +1,20 @@
|
||||
class CefrMapper:
|
||||
"""Maps IRT theta values to CEFR levels and IELTS band scores."""
|
||||
"""Thin re-export of the canonical CEFR mapper for backward compat.
|
||||
|
||||
THETA_TO_CEFR = [
|
||||
(-4.0, -2.5, 'pre_a1'),
|
||||
(-2.5, -1.5, 'a1'),
|
||||
(-1.5, -0.5, 'a2'),
|
||||
(-0.5, 0.5, 'b1'),
|
||||
(0.5, 1.5, 'b2'),
|
||||
(1.5, 2.5, 'c1'),
|
||||
(2.5, 4.0, 'c2'),
|
||||
]
|
||||
The canonical implementation lives in ``encoach_ai.services.cefr_mapper``.
|
||||
This shim is kept so existing imports like
|
||||
``from odoo.addons.encoach_placement.services.cefr_mapper import CefrMapper``
|
||||
continue to work. See P0.9 in §21 of docs/PROJECT_SUMMARY.md.
|
||||
"""
|
||||
|
||||
CEFR_TO_BAND = {
|
||||
'pre_a1': 2.0,
|
||||
'a1': 3.0,
|
||||
'a2': 4.0,
|
||||
'b1': 5.0,
|
||||
'b2': 6.5,
|
||||
'c1': 7.5,
|
||||
'c2': 9.0,
|
||||
}
|
||||
|
||||
CEFR_LABELS = {
|
||||
'pre_a1': 'Pre-A1 (Beginner)',
|
||||
'a1': 'A1 (Elementary)',
|
||||
'a2': 'A2 (Pre-Intermediate)',
|
||||
'b1': 'B1 (Intermediate)',
|
||||
'b2': 'B2 (Upper-Intermediate)',
|
||||
'c1': 'C1 (Advanced)',
|
||||
'c2': 'C2 (Proficient)',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def theta_to_cefr(theta):
|
||||
for low, high, level in CefrMapper.THETA_TO_CEFR:
|
||||
if low <= theta < high:
|
||||
return level
|
||||
return 'c2' if theta >= 2.5 else 'pre_a1'
|
||||
|
||||
@staticmethod
|
||||
def theta_to_band(theta):
|
||||
cefr = CefrMapper.theta_to_cefr(theta)
|
||||
base_band = CefrMapper.CEFR_TO_BAND.get(cefr, 5.0)
|
||||
|
||||
for low, high, level in CefrMapper.THETA_TO_CEFR:
|
||||
if level == cefr:
|
||||
range_width = high - low
|
||||
if range_width > 0:
|
||||
position = (theta - low) / range_width
|
||||
else:
|
||||
position = 0.5
|
||||
|
||||
cefr_list = list(CefrMapper.CEFR_TO_BAND.keys())
|
||||
idx = cefr_list.index(cefr)
|
||||
next_band = CefrMapper.CEFR_TO_BAND.get(
|
||||
cefr_list[min(idx + 1, len(cefr_list) - 1)], base_band + 1.0
|
||||
)
|
||||
|
||||
band = base_band + position * (next_band - base_band)
|
||||
return round(band * 2) / 2
|
||||
|
||||
return base_band
|
||||
|
||||
@staticmethod
|
||||
def band_to_cefr(band):
|
||||
if band < 2.5:
|
||||
return 'pre_a1'
|
||||
if band < 3.5:
|
||||
return 'a1'
|
||||
if band < 4.5:
|
||||
return 'a2'
|
||||
if band < 5.5:
|
||||
return 'b1'
|
||||
if band < 7.0:
|
||||
return 'b2'
|
||||
if band < 8.0:
|
||||
return 'c1'
|
||||
return 'c2'
|
||||
|
||||
@staticmethod
|
||||
def get_cefr_label(cefr_code):
|
||||
return CefrMapper.CEFR_LABELS.get(cefr_code, cefr_code)
|
||||
from odoo.addons.encoach_ai.services.cefr_mapper import ( # noqa: F401
|
||||
CefrMapper,
|
||||
band_to_cefr,
|
||||
theta_to_cefr,
|
||||
theta_to_band,
|
||||
cefr_to_band,
|
||||
normalize_cefr,
|
||||
cefr_label,
|
||||
THETA_TO_CEFR,
|
||||
CEFR_TO_BAND,
|
||||
CEFR_LABELS,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user