V2
Browse files- Procfile +1 -0
- README.md +0 -12
- api/__pycache__/main.cpython-312.pyc +0 -0
- api/main.py +32 -59
- app.py +5 -0
- debug.py +9 -0
- vercel.json +0 -7
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: uvicorn api.main:app --host 0.0.0.0 --port ${PORT:-8000}
|
README.md
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Test R
|
| 3 |
-
emoji: 🏢
|
| 4 |
-
colorFrom: yellow
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 5.49.1
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/__pycache__/main.cpython-312.pyc
CHANGED
|
Binary files a/api/__pycache__/main.cpython-312.pyc and b/api/__pycache__/main.cpython-312.pyc differ
|
|
|
api/main.py
CHANGED
|
@@ -436,10 +436,18 @@ def match_offer_sync(offer_text: str, top_k: int = 7, with_explanation: bool = T
|
|
| 436 |
|
| 437 |
# Calculer un score d'expérience (toujours, utilisé pour le score final)
|
| 438 |
if required_exp is not None:
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
else:
|
| 442 |
-
#
|
| 443 |
exp_score = min(1.0, profile_exp / 20)
|
| 444 |
|
| 445 |
# Générer l'explication (optionnel)
|
|
@@ -447,11 +455,26 @@ def match_offer_sync(offer_text: str, top_k: int = 7, with_explanation: bool = T
|
|
| 447 |
if with_explanation:
|
| 448 |
explanation = generate_explanation(offer_text, row, skills_score, exp_score)
|
| 449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
# Calculer un score de pertinence combiné (compétences + expérience)
|
| 451 |
try:
|
| 452 |
-
|
| 453 |
except Exception:
|
| 454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
candidates.append({
|
| 457 |
'profile': ProfileResult(
|
|
@@ -476,61 +499,11 @@ def match_offer_sync(offer_text: str, top_k: int = 7, with_explanation: bool = T
|
|
| 476 |
# 4) Ensuite trier par expérience décroissante. Si required_exp est présent dans l'offre, on place d'abord les profils
|
| 477 |
# avec profile_exp >= required_exp (triés desc), puis compléter avec les autres (triés desc)
|
| 478 |
|
| 479 |
-
#
|
| 480 |
-
|
| 481 |
-
without_skills = [c for c in candidates if c['skills_match_count'] == 0]
|
| 482 |
-
|
| 483 |
-
# Trier ceux avec compétences : on applique les priorités successives
|
| 484 |
-
def sort_key(c):
|
| 485 |
-
# role_match True -> come first (-1), location_match True -> come first (-1)
|
| 486 |
-
return (
|
| 487 |
-
-c['skills_match_count'],
|
| 488 |
-
-int(c['role_match']),
|
| 489 |
-
-int(c['location_match']),
|
| 490 |
-
-c['profile_exp']
|
| 491 |
-
)
|
| 492 |
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
# S'assurer du comportement demandé par le recruteur : si l'offre précise un seuil d'expérience,
|
| 496 |
-
# prioriser profils >= seuil (triés par expérience décroissante), puis compléter par les autres.
|
| 497 |
-
ordered = []
|
| 498 |
-
if required_exp is not None:
|
| 499 |
-
meets_exp = [c for c in with_skills if c['profile_exp'] >= required_exp]
|
| 500 |
-
not_meets_exp = [c for c in with_skills if c['profile_exp'] < required_exp]
|
| 501 |
-
# Trier par : skills_match_count, role_match, location_match, puis expérience décroissante
|
| 502 |
-
meets_exp.sort(key=lambda c: (
|
| 503 |
-
-c['skills_match_count'],
|
| 504 |
-
-int(c['role_match']),
|
| 505 |
-
-int(c['location_match']),
|
| 506 |
-
-c['profile_exp']
|
| 507 |
-
))
|
| 508 |
-
not_meets_exp.sort(key=lambda c: (
|
| 509 |
-
-c['skills_match_count'],
|
| 510 |
-
-int(c['role_match']),
|
| 511 |
-
-int(c['location_match']),
|
| 512 |
-
-c['profile_exp']
|
| 513 |
-
))
|
| 514 |
-
ordered.extend(meets_exp)
|
| 515 |
-
ordered.extend(not_meets_exp)
|
| 516 |
-
else:
|
| 517 |
-
# Pas de seuil : on veut principalement trier par expérience décroissante, mais en privilégiant
|
| 518 |
-
# d'abord ceux qui matchent les compétences / rôle / localisation.
|
| 519 |
-
with_skills.sort(key=lambda c: (
|
| 520 |
-
-int(c['role_match']),
|
| 521 |
-
-int(c['location_match']),
|
| 522 |
-
-c['skills_match_count'],
|
| 523 |
-
-c['profile_exp']
|
| 524 |
-
))
|
| 525 |
-
ordered.extend(with_skills)
|
| 526 |
-
|
| 527 |
-
# Si on manque de profils pour top_k, on complète avec candidats sans skills (triés par role/location/exp)
|
| 528 |
-
if len(ordered) < top_k:
|
| 529 |
-
without_skills.sort(key=sort_key)
|
| 530 |
-
ordered.extend(without_skills)
|
| 531 |
-
|
| 532 |
-
# Enfin retourner les top_k profils (convertis en ProfileResult)
|
| 533 |
-
return [c['profile'] for c in ordered[:top_k]]
|
| 534 |
|
| 535 |
# --- Endpoints de l'API ---
|
| 536 |
@app.get("/")
|
|
|
|
| 436 |
|
| 437 |
# Calculer un score d'expérience (toujours, utilisé pour le score final)
|
| 438 |
if required_exp is not None:
|
| 439 |
+
if profile_exp >= required_exp:
|
| 440 |
+
# L'expérience est suffisante ou supérieure, le score est élevé
|
| 441 |
+
# Bonus pour l'expérience supplémentaire, plafonné pour ne pas surpondérer
|
| 442 |
+
exp_score = min(1.0, 0.8 + (profile_exp - required_exp) * 0.05)
|
| 443 |
+
else:
|
| 444 |
+
# L'expérience est inférieure, le score est proportionnel
|
| 445 |
+
if required_exp > 0:
|
| 446 |
+
exp_score = max(0, (profile_exp / required_exp) * 0.7)
|
| 447 |
+
else:
|
| 448 |
+
exp_score = 0
|
| 449 |
else:
|
| 450 |
+
# Pas d'exigence, on normalise sur une échelle de 20 ans
|
| 451 |
exp_score = min(1.0, profile_exp / 20)
|
| 452 |
|
| 453 |
# Générer l'explication (optionnel)
|
|
|
|
| 455 |
if with_explanation:
|
| 456 |
explanation = generate_explanation(offer_text, row, skills_score, exp_score)
|
| 457 |
|
| 458 |
+
# Pondération fixe 50% compétences / 50% expérience, comme demandé
|
| 459 |
+
skills_weight = 0.5
|
| 460 |
+
exp_weight = 0.5
|
| 461 |
+
|
| 462 |
# Calculer un score de pertinence combiné (compétences + expérience)
|
| 463 |
try:
|
| 464 |
+
base_score = calculate_weighted_score(skills_score, exp_score, skills_weight=skills_weight, exp_weight=exp_weight)
|
| 465 |
except Exception:
|
| 466 |
+
base_score = 0.0
|
| 467 |
+
|
| 468 |
+
# Petites primes pour role_match / location_match / nombre de skills matchés
|
| 469 |
+
bonus = 0.0
|
| 470 |
+
if role_match:
|
| 471 |
+
bonus += 0.08
|
| 472 |
+
if location_match:
|
| 473 |
+
bonus += 0.04
|
| 474 |
+
# bonus croissant mais plafonné pour skills_match_count
|
| 475 |
+
bonus += min(0.03 * skills_match_count, 0.12)
|
| 476 |
+
|
| 477 |
+
final_score = min(1.0, base_score + bonus)
|
| 478 |
|
| 479 |
candidates.append({
|
| 480 |
'profile': ProfileResult(
|
|
|
|
| 499 |
# 4) Ensuite trier par expérience décroissante. Si required_exp est présent dans l'offre, on place d'abord les profils
|
| 500 |
# avec profile_exp >= required_exp (triés desc), puis compléter avec les autres (triés desc)
|
| 501 |
|
| 502 |
+
# Trier tous les candidats par final_score décroissant (affichage demandé : ordre par pourcentage)
|
| 503 |
+
candidates.sort(key=lambda c: -c.get('profile').score)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
+
# Retourner les top_k profils
|
| 506 |
+
return [c['profile'] for c in candidates[:top_k]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
# --- Endpoints de l'API ---
|
| 509 |
@app.get("/")
|
app.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from api.main import app
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
import uvicorn
|
| 5 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
debug.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import traceback
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
print("Attempting to import main.py...")
|
| 5 |
+
from api import main
|
| 6 |
+
print("Successfully imported main.py")
|
| 7 |
+
except Exception as e:
|
| 8 |
+
print("Failed to import main.py. See traceback below:")
|
| 9 |
+
traceback.print_exc()
|
vercel.json
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"functions": {
|
| 3 |
-
"api/index.py": {
|
| 4 |
-
"runtime": "python3.12"
|
| 5 |
-
}
|
| 6 |
-
}
|
| 7 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|