| from pathlib import Path |
| import sys |
|
|
| backend_root = Path(__file__).resolve().parents[1] |
| if str(backend_root) not in sys.path: |
| sys.path.insert(0, str(backend_root)) |
|
|
| from ai_module.chatbot.conversation_memory import ConversationMemory |
| from ai_module.nlp.multilingual_skill_extractor import MultilingualSkillExtractor |
| from scripts.prepare_ner_annotations import prepare_annotations, normalize_spans, spans_to_bio |
| from datetime import datetime |
|
|
| from sqlalchemy import create_engine |
| from sqlalchemy.orm import sessionmaker |
|
|
| from app.core.database import Base |
| from app.models.models import User, Candidate, JobCriteria, RecruiterFeedback |
| from ai_module.feedback.recruiter_feedback import RecruiterFeedbackEngine |
|
|
|
|
| class FakeRedisClient: |
| def __init__(self): |
| self.lists = {} |
| self.hashes = {} |
|
|
| def ping(self): |
| return True |
|
|
| def rpush(self, key, value): |
| self.lists.setdefault(key, []).append(value) |
|
|
| def ltrim(self, key, start, end): |
| self.lists[key] = self.lists.get(key, [])[start : end + 1 if end != -1 else None] |
|
|
| def expire(self, key, ttl): |
| return None |
|
|
| def hset(self, key, mapping): |
| self.hashes.setdefault(key, {}).update(mapping) |
|
|
| def hget(self, key, field): |
| return self.hashes.get(key, {}).get(field) |
|
|
| def delete(self, *keys): |
| for key in keys: |
| self.lists.pop(key, None) |
| self.hashes.pop(key, None) |
|
|
| def lrange(self, key, start, end): |
| return self.lists.get(key, [])[start : end + 1 if end != -1 else None] |
|
|
| def keys(self, pattern): |
| prefix = pattern[:-1] if pattern.endswith("*") else pattern |
| return [key for key in self.lists if key.startswith(prefix)] |
|
|
|
|
| if __name__ == "__main__": |
| memory = ConversationMemory(client=FakeRedisClient()) |
| memory.add_message("a", "user", "bonjour") |
| memory.add_message("a", "assistant", "salut") |
| memory.add_message("b", "user", "hola") |
| assert [item["content"] for item in memory.get_history("a")] == ["bonjour", "salut"] |
| assert [item["content"] for item in memory.get_history("b")] == ["hola"] |
| assert set(memory.list_sessions()) == {"a", "b"} |
|
|
| extractor = MultilingualSkillExtractor() |
| french = extractor.extract_skills( |
| "Développeur Python avec expérience en apprentissage automatique, Docker, communication et français courant." |
| ) |
| assert {"Python", "Machine Learning", "Docker", "Communication", "French"}.issubset({item["name"] for item in french}) |
|
|
| spanish = extractor.extract_skills("Ingeniero de datos con aprendizaje automático, SQL, Docker y español fluido.") |
| assert {"Machine Learning", "SQL", "Docker", "Spanish"}.issubset({item["name"] for item in spanish}) |
|
|
| template = prepare_annotations([{"text": "Python developer at ACME"}], mode="template") |
| assert template[0]["tokens"] == ["Python", "developer", "at", "ACME"] |
| assert template[0]["ner_tags"] == ["O", "O", "O", "O"] |
|
|
| spans = normalize_spans([ |
| {"start": 7, "end": 13, "label": "SKILL"}, |
| {"start": 27, "end": 31, "label": "ORG"}, |
| ]) |
| bio = spans_to_bio("Senior Python developer at ACME", spans) |
| assert bio["ner_tags"][1] == "B-SKILL" |
| assert bio["ner_tags"][4] == "B-ORG" |
|
|
| engine = create_engine("sqlite:///:memory:") |
| Base.metadata.create_all(bind=engine) |
| session = sessionmaker(bind=engine)() |
|
|
| recruiter = User(email="recruiter@example.com", hashed_password="x", full_name="Recruiter") |
| candidate = Candidate(full_name="Alice Smith", email="alice@example.com", raw_text="Python FastAPI Docker") |
| criteria = JobCriteria(recruiter_id=1, title="Senior Python Developer", description="Need Python and Docker") |
| session.add_all([recruiter, candidate, criteria]) |
| session.flush() |
| session.add_all([ |
| RecruiterFeedback( |
| criteria_id=criteria.id, |
| candidate_id=candidate.id, |
| recruiter_id=recruiter.id, |
| model_predicted_score=62.0, |
| model_predicted_decision="review", |
| recruiter_decision="accepted", |
| recruiter_score_override=85.0, |
| feedback_reason="Strong interview", |
| is_override=True, |
| created_at=datetime.utcnow(), |
| ), |
| RecruiterFeedback( |
| criteria_id=criteria.id, |
| candidate_id=candidate.id, |
| recruiter_id=recruiter.id, |
| model_predicted_score=45.0, |
| model_predicted_decision="rejected", |
| recruiter_decision="rejected", |
| is_override=False, |
| created_at=datetime.utcnow(), |
| ), |
| ]) |
| session.commit() |
|
|
| feedback = RecruiterFeedbackEngine(session) |
| stats = feedback.get_override_statistics() |
| assert stats["total_feedback"] == 2 |
| assert stats["override_count"] == 1 |
| assert stats["override_rate"] == 50.0 |
| assert feedback.get_retraining_readiness(min_samples=2, min_override_rate=25.0)["ready"] is True |
| assert feedback.summarize_by_criteria()[criteria.id]["total"] == 2 |
| assert feedback.prepare_retraining_dataset(min_samples=2) |
|
|
| print("phase2 smoke ok") |
|
|