
    hix1                         d Z ddlZddlZddlZddlmZmZ ddlZddlZddl	m
Z ddlZddlZddlmZ  ej                   e      Z e         G d d      Z e       Zy)z-
Service de parsing de CV avec Google Gemini
    N)DictTuple)load_dotenvc                       e Zd ZdZh dZd Zdedeeef   fdZ	de
defdZdedefd	Zde
defd
Zde
defdZdedefdZdedefdZdedefdZdefdZdededeeef   fdZy)CVParserServicezDService pour parser et extraire les informations des CVs avec Gemini>%      tâches   diplôme   expérience   université   compétences   réalisations   responsabilitéscurriculum vitaecvpostestagetasksdegreeprofilprojetresumeskillscompanydiplomelanguesmissionprofileprojectobjectifposition	education	formation	languages	objective
entreprise
experience
internship
universitycompetencesachievementscertificationresponsibilitiesc                     t        j                  d      }|st        d      t        j                  |       t        j
                  d      | _        t        j                  d       y )NGEMINI_API_KEYz"GEMINI_API_KEY manquante dans .env)api_keyzgemini-flash-latestu4   ✅ Service Gemini initialisé (gemini-flash-latest))	osgetenv
ValueErrorgenai	configureGenerativeModelmodelloggerinfo)selfgemini_api_keys     >/home/www/therecruiter.miabetepe.com/app/services/cv_parser.py__init__zCVParserService.__init__#   sN    #34ABB/ **+@A
JK    textreturnc                 R  	 |rt        |      dk  ry|j                         	t        	fd| j                  D              }t	        t        j                  d|            }t	        t        j                  d|            }t	        t        j                  d|            }d}g }|dk\  r|d	z  }|j                  | d
       n=|dk\  r|dz  }|j                  | d
       n|dk\  r|dz  }|j                  | d       |r|dz  }|j                  d       |r|dz  }|j                  d       |r|dz  }|j                  d       |dk\  r!t        j                  d| d       dd| dfS d| d}||rddj                  |      z   ndz  }t        j                  d|        d|fS ) uw   
        Vérifie si le texte extrait ressemble à un CV
        
        Retourne: (is_cv: bool, reason: str)
        d   )FuF   Le document est trop court pour être un CV (minimum 100 caractères).c              3   ,   K   | ]  }|v sd   yw)   N ).0keyword
text_lowers     r;   	<genexpr>z/CVParserService.is_likely_cv.<locals>.<genexpr><   s     X7'ZBWQXs   	z.[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}zC[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}z\b(19|20)\d{2}\br      (   u    mots-clés CV détectés      rC   
   u    mot(s)-clé(s) CV détecté(s)   u   Email détectéu   Téléphone détectéu   Dates/années détectées2   u&   ✅ Document validé comme CV (score: z/100)TzCV valide (score: u.   Ce document ne semble pas être un CV (score: z/100). u   Raisons détectées: z, u   Aucun indicateur CV trouvé.u   ⚠️ F)lenlowersumCV_KEYWORDSboolresearchappendr7   r8   joinwarning)
r9   r>   keywords_found	has_email	has_phone	has_datesscorereasonsreasonrG   s
            @r;   is_likely_cvzCVParserService.is_likely_cv0   s    s4y3bZZ\
 Xd.>.>XX #TVZ[\	#ikopq	#6=>	  QRKENNn--FGHq RKENNn--FGHq RKENNn--LMN RKENN,- RKENN23 RKENN67 B;KK@uMN-eWE:::EeWGTFg-		'0BBSqqFNNWVH-.&= r=   pdf_contentc                    	 | j                  |      }|rt        |      dk  r| j                  |      }|rt        |      dk  rt        d      t        j                  dt        |       d       | j                  |      \  }}|st        |      | j                  |      }d|i|S # t        $ r}t        j                  d|         d}~wt        $ r}t        j                  d	|         d}~ww xY w)
z(Parse un fichier PDF et extrait le texterA   rO   z%Impossible d'extraire du texte du PDFu   📄 PDF parsé: u    caractères extraitsraw_textu   ❌ Validation échouée: Nu   ❌ Erreur parsing PDF: )
_extract_with_pypdf2rP   _extract_with_pdfplumberr2   r7   r8   ra   extract_structured_dataerror	Exception)r9   rb   rd   is_cvr`   structured_dataes          r;   	parse_pdfzCVParserService.parse_pdfk   s    	00=H s8}s288Es8}r1 !HIIKK+CM?:OPQ !--h7ME6 (( #::8DO H! 
  	LL5aS9: 	LL3A378	s$   B&B) )	C52CC5C00C5c                 .    |s|S |j                  dd      S )uQ   Supprime les caractères nuls et autres caractères non supportés par PostgreSQL  )replacer9   r>   s     r;   _sanitize_textzCVParserService._sanitize_text   s    K||FB''r=   c                 D   	 t        j                  |      }t        j                  |      }d}|j                  D ]  }||j                         dz   z  } | j                  |j                               S # t        $ r"}t        j                  d|        Y d}~yd}~ww xY w)zExtraction avec PyPDF2rp   
u   ⚠️ Erreur PyPDF2: N)ioBytesIOPyPDF2	PdfReaderpagesextract_textrs   stripri   r7   rY   )r9   rb   pdf_file
pdf_readerr>   pagerl   s          r;   re   z$CVParserService._extract_with_pypdf2   s    	zz+.H))(3JD"(( 3))+d223 &&tzz|44 	NN3A378	s   A1A4 4	B=BBc                 z   	 t        j                  |      }d}t        j                  |      5 }|j                  D ]  }|j                         }|s||dz   z  } 	 ddd       | j                  |j                               S # 1 sw Y   (xY w# t        $ r"}t        j                  d|        Y d}~yd}~ww xY w)zExtraction avec pdfplumberrp   ru   Nu   ⚠️ Erreur pdfplumber: )rv   rw   
pdfplumberopenrz   r{   rs   r|   ri   r7   rY   )r9   rb   r}   r>   pdfr   	page_textrl   s           r;   rf   z(CVParserService._extract_with_pdfplumber   s    	zz+.HD* 1cII 1D $ 1 1 3I 	D 0011 &&tzz|441 1  	NN7s;<	s4   ,B "B
B&B BB 	B:B55B:rd   c           
      v   	 |dd }d| d}d}t        |      D ]/  }	 | j                  j                  |t        j                  j                  dddd	
            }|j                  j                         }| j                  |      }t        j                  |      }t        j                  d       t        j                  dt        |j                  dg              d       t        j                  dt        |j                  dg              d       t        j                  dt        |j                  dg              d       |c S  y# t        j                  $ r}	||dz
  k  r"t        j!                  d|dz    d       Y d}	~	qt        j#                  d| d|	        t        j#                  ddd  d       | j%                  |      }
|
r	|
cY d}	~	c S | j'                         cY d}	~	c S d}	~	ww xY w# t(        $ r2}	t        j#                  d|	        | j'                         cY d}	~	S d}	~	ww xY w)uA   Utilise Google Gemini pour extraire les informations structuréesNi.  z?Analyse ce CV et extrais les informations en JSON strict.

CV:
uJ  

Tu dois retourner UNIQUEMENT un JSON valide avec cette structure exacte:
{
  "skills": ["compétence1", "compétence2", "compétence3"],
  "experience": [
    {
      "company": "nom entreprise",
      "position": "poste occupé",
      "duration": "période (ex: 2020-2023)",
      "description": "description courte SANS guillemets"
    }
  ],
  "education": [
    {
      "institution": "nom établissement",
      "degree": "diplôme obtenu",
      "year": "année"
    }
  ],
  "languages": ["langue1", "langue2"],
  "location": "ville, pays (ex: Paris, France) ou chaîne vide si non trouvé",
  "summary": "résumé professionnel en 2-3 phrases"
}

Règles CRITIQUES:
- Retourne UNIQUEMENT le JSON, aucun texte avant ou après
- Pas de markdown, pas de backticks
- JAMAIS de guillemets " à l'intérieur des valeurs de chaînes
- Remplace tous les guillemets dans les descriptions par des apostrophes '
- Si une information manque, mets un tableau vide [] ou une chaîne vide ""
- Maximum 15 compétences les plus pertinentes
- Les descriptions doivent être courtes (max 100 caractères)
   g?gffffff?rJ   i'  )temperaturetop_ptop_kmax_output_tokens)generation_configu2   ✅ Données CV structurées extraites avec Geminiz   - r   u    compétencesr&   u    expériencesr!   z formationsrC   u   ⚠️ Tentative u    échouée, réessai...u)   ❌ Erreur parsing JSON de Gemini après z tentatives: u   Réponse brute: i  z...u#   ❌ Erreur extraction avec Gemini: )ranger6   generate_contentr3   typesGenerationConfigr>   r|   _clean_json_responsejsonloadsr7   r8   rP   getJSONDecodeErrorrY   rh   _try_repair_json_get_default_structureri   )r9   rd   text_to_analyzepromptmax_retriesattemptresponseresult_textresultrl   repaireds              r;   rg   z'CVParserService.extract_structured_data   s$   Y	1&v.O   !$FN K - )=(=#zz::*/++*F*F(+"&"$.3	 +G +  ;  H #+--"5"5"7K #'";";K"HK "ZZ4FKK TUKK%FJJx,D(E'Fm TUKK%FJJ|R,H(I'J- XYKK%FJJ{B,G(H'I UV!M5)=8 ++ =q0):7Q;-G^'_` 'PQ\P]]jkljm%no'7DS8I7J#%NO $(#8#8#E##+O#::<<=  	1LL>qcBC..00	1sr   G= D*E	G= G= G:!$G5G= AG5G:G= G5-G:.G= 5G::G= =	H8'H3-H83H8c                     |j                  d      r2|j                  dd      j                  dd      j                         }|S |j                  d      r |j                  dd      j                         }|S )u"   Nettoie la réponse JSON de Geminiz```jsonrp   z```)
startswithrq   r|   rr   s     r;   r   z$CVParserService._clean_json_response  si     ??9%<<	2.66ubAGGID  __U#<<r*002Dr=   broken_jsonc                    	 t        j                  d|t         j                        }|r|j                  d      }|j	                  d      D cg c]0  }|j                         j                  d      j                  d      2 }}|D cg c]  }|s|	 }}t        j                  d       |dd g g g d	d
S 	 yc c}w c c}w # t        $ r"}t        j                  d|        Y d}~yd}~ww xY w)u    Tente de réparer un JSON casséz"skills":\s*\[(.*?)\]rC   ,"'u4   ✅ Récupération partielle: compétences extraitesN   u?   CV analysé partiellement. Compétences extraites avec succès.)r   r&   r!   r#   summaryu   ❌ Échec réparation JSON: )
rU   rV   DOTALLgroupsplitr|   r7   r8   ri   rh   )r9   r   skills_match
skills_strsr   rl   s          r;   r   z CVParserService._try_repair_json  s    	>99%={BIIVL)//2
CMCSCSTWCXYa!'')//#.44S9YY%+1q!11RS$Sbk"$!#!#`  "  Z1  	>LL8<==	>s<   AC 5B8C B=B=!C 8
C 	C-C((C-c                     g g g g dddS )u%   Structure par défaut en cas d'erreurrp   u6   Extraction automatique échouée. CV reçu et stocké.)r   r&   r!   r#   locationr   rD   )r9   s    r;   r   z&CVParserService._get_default_structure4  s      O
 	
r=   content_type	file_sizec                 $    dg}||vryd}||kD  ryy)zValide le fichier CVzapplication/pdf)Fu7   ❌ Format non supporté. Seuls les PDF sont acceptés.i   )Fu(   ❌ Fichier trop volumineux. Max: 10 MB.)TOKrD   )r9   r   r   allowed_typesmax_sizes        r;   validate_cv_filez CVParserService.validate_cv_file?  s(    *+},S#xDr=   N)__name__
__module____qualname____doc__rS   r<   strr   rT   ra   bytesr   rm   rs   re   rf   rg   r   r   r   inttupler   rD   r=   r;   r   r      s    NKL9! 9!tSy)9 9!v"U "t "H(3 (3 ( # E c "[1 [1 [1z  C D 0	
 	

S 
S 
U4QT9EU 
r=   r   )r   rv   r   loggingtypingr   r   rx   r   google.generativeaigenerativeair3   r0   rU   dotenvr   	getLoggerr   r7   r   cv_parser_servicerD   r=   r;   <module>r      sY    
      # 	 	 			8	$ t tp	 $% r=   