Files
encoach_backend_v4/custom_addons/encoach_vector/models/embedding.py
Yamen Ahmad 6ec68160c8 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

123 lines
4.2 KiB
Python

"""Odoo model for storing vector embeddings via pgvector."""
import json
import logging
from odoo import api, models, fields
_logger = logging.getLogger(__name__)
VECTOR_DIM = 384 # all-MiniLM-L6-v2 output dimension
class EncoachEmbedding(models.Model):
_name = 'encoach.embedding'
_description = 'Vector Embedding'
_order = 'create_date desc'
content_type = fields.Selection([
('course', 'Course'),
('resource', 'Resource'),
('question', 'Question'),
('module', 'Module'),
('topic', 'Topic'),
('feedback', 'Feedback'),
('generation_log', 'Generation Log'),
('material', 'Course Material'),
], required=True, index=True)
content_id = fields.Integer(required=True, index=True)
content_text = fields.Text()
metadata_json = fields.Text(default='{}')
_content_unique = models.Constraint(
'UNIQUE(content_type, content_id)',
'Each content item can only have one embedding.',
)
@api.model
def _auto_init(self):
res = super()._auto_init()
cr = self.env.cr
cr.execute("SELECT 1 FROM pg_extension WHERE extname = 'vector'")
if not cr.fetchone():
try:
cr.execute("CREATE EXTENSION IF NOT EXISTS vector")
_logger.info("pgvector extension created")
except Exception:
_logger.warning(
"Could not create pgvector extension — run "
"'CREATE EXTENSION vector' as a superuser",
exc_info=True,
)
return res
cr.execute("""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'encoach_embedding' AND column_name = 'embedding'
""")
if not cr.fetchone():
cr.execute(
f"ALTER TABLE encoach_embedding ADD COLUMN embedding vector({VECTOR_DIM})"
)
cr.execute(
"CREATE INDEX IF NOT EXISTS encoach_embedding_vec_idx "
"ON encoach_embedding USING ivfflat (embedding vector_cosine_ops) "
"WITH (lists = 100)"
)
_logger.info("Vector column and index created on encoach_embedding")
return res
def set_embedding(self, vector):
"""Store a vector embedding for this record."""
self.ensure_one()
vec_str = '[' + ','.join(str(v) for v in vector) + ']'
self.env.cr.execute(
"UPDATE encoach_embedding SET embedding = %s WHERE id = %s",
(vec_str, self.id),
)
@api.model
def cron_reindex(self):
"""Cron entry point for periodic re-indexing."""
from odoo.addons.encoach_vector.services.indexer import index_all
return index_all(self.env)
@api.model
def similarity_search(self, query_vector, *, content_type=None, limit=10):
"""Find similar embeddings using cosine distance."""
vec_str = '[' + ','.join(str(v) for v in query_vector) + ']'
where = "WHERE embedding IS NOT NULL"
params = [vec_str, limit]
if content_type:
where += " AND content_type = %s"
params = [vec_str, content_type, limit]
query = f"""
SELECT id, content_type, content_id, content_text, metadata_json,
1 - (embedding <=> %s::vector) AS similarity
FROM encoach_embedding
{where}
ORDER BY embedding <=> %s::vector
LIMIT %s
"""
if content_type:
self.env.cr.execute(query, (vec_str, content_type, vec_str, limit))
else:
self.env.cr.execute(query, (vec_str, vec_str, limit))
results = []
for row in self.env.cr.dictfetchall():
metadata = {}
try:
metadata = json.loads(row['metadata_json'] or '{}')
except (json.JSONDecodeError, TypeError):
pass
results.append({
'id': row['id'],
'content_type': row['content_type'],
'content_id': row['content_id'],
'text': row['content_text'],
'metadata': metadata,
'similarity': round(row['similarity'], 4),
})
return results