"""
return results_html
# ---------------------------------------------------------------------------
# RAPPORT HTML (remplace PDF — imprimable via Ctrl+P)
# ---------------------------------------------------------------------------
def generate_html_report(combined_query, keywords, results, llm_context, synthesis):
"""
Appelle le LLM avec un prompt dédié 'rapport RH' puis génère un HTML
formaté, imprimable via Ctrl+P ou Enregistrer en PDF depuis le navigateur.
"""
now = datetime.now().strftime("%d/%m/%Y à %Hh%M")
llm_query = combined_query[:2000]
# Appel LLM dédié rapport RH (synchrone, sans streaming)
print("📝 Génération de la note de synthèse RH...")
try:
rh_messages = [
{
"role": "system",
"content": (
"Tu es un consultant RH senior à l'OPT-NC. "
"Tu rédiges des notes de synthèse officielles destinées "
"à être archivées dans le dossier d'un candidat. "
"Ton style est professionnel, structuré et factuel. "
"Réponds en HTML avec des balises
,
,
,
, . "
"N'inclus pas de balises , ou ."
)
},
{
"role": "user",
"content": f"""Rédige une note de synthèse RH structurée pour le candidat suivant.
PROFIL CANDIDAT :
{llm_query}
MOTS-CLÉS MÉTIER IDENTIFIÉS (query expansion) :
{keywords if keywords else "Non disponibles"}
MÉTIERS SUGGÉRÉS PAR LE SYSTÈME RAG :
{llm_context}
La note doit comporter exactement ces 4 sections avec des balises
:
1. Profil synthétique — résumé du candidat en 3-4 lignes
2. Adéquation aux métiers identifiés — pour chaque métier, les points de correspondance
3. Points de vigilance — compétences manquantes ou écarts à combler
4. Recommandations — formations ou étapes suggérées
Ne dépasse pas 400 mots. Réponds en français."""
}
]
rh_completion = client.chat_completion(rh_messages, max_tokens=800)
rh_html = rh_completion.choices[0].message.content
# Nettoyer les éventuels blocs ```html
rh_html = re.sub(r'```html?', '', rh_html)
rh_html = re.sub(r'```', '', rh_html).strip()
except Exception as e:
rh_html = f"
Erreur lors de la génération de la note : {e}
"
# Construction des métiers suggérés en HTML
metiers_html = ""
for i, res in enumerate(results):
m = res["metier"]
score = res["score"] * 100
metiers_html += f"""
{i+1}. {m['title']}
Match : {score:.1f}%
Famille : {m['famille']} | ID : {str(m['id']).split('_')[0]}
"""
# Synthèse grand public (déjà streamée) en HTML
synthesis_html = md.render(synthesis) if synthesis else "
Non disponible
"
# Assemblage du HTML interne (dans l'iframe)
inner_html = f"""
OPT-NC — Note de synthèse RH
Générée le {now}
Profil candidat
{llm_query}
{"" if not keywords else f'''
Mots-clés métier identifiés
{keywords}
'''}
Métiers suggérés par le système RAG
{metiers_html}
Analyse RH (générée par IA — usage interne)
{rh_html}
Synthèse présentée au candidat
{synthesis_html}
Transparence & mentions
Ce document a été produit automatiquement par un prototype opérationnel
développé par l'OPT-NC, à partir de données ouvertes et de
modèles open source.
Données
Référentiel GPEC OPT-NC (open data)
Recherche sémantique
{MODEL_ID} (open source)
Modèle d'analyse
{LLM_MODEL} (open source)
Fiches indexées
{len(df)}
⚠️ Les suggestions sont indicatives et ne constituent pas une décision RH officielle.
"""
import base64
encoded = base64.b64encode(inner_html.encode('utf-8')).decode('utf-8')
html = f''
return html
# ---------------------------------------------------------------------------
# RECHERCHE + CHAT
# ---------------------------------------------------------------------------
def search_and_chat(query, file_obj, top_k, score_min):
"""Générateur : yield (results_html, llm_text, results_state, query_state, keywords_state, context_state, synthesis_state)."""
combined_query = query
if file_obj is not None:
pdf_text = extract_text_from_pdf(file_obj.name)
if pdf_text.startswith("Erreur"):
yield results_html_error(pdf_text), "Erreur lors de la lecture du CV.", [], "", "", "", ""
return
combined_query = f"{query}\n\n[Contenu du CV extrait] :\n{pdf_text}" if query else pdf_text
if not combined_query:
yield "Veuillez entrer une description ou uploader un CV.", "En attente de votre profil...", [], "", "", "", ""
return
# Query expansion
expanded_query, keywords = expand_query(combined_query)
results, context = get_rag_context(expanded_query, top_k, score_min / 100)
if not results:
seuil = int(score_min)
yield (
f"
"
f"⚠️ Aucun métier trouvé avec un score ≥ {seuil}%. "
f"Essayez de baisser le seuil de score minimum.
",
"Aucun résultat suffisamment pertinent trouvé.", [], "", "", "", ""
)
return
# Fiches HTML immédiates
results_html = build_results_html(results)
yield results_html, "⏳ Analyse en cours...", results, combined_query, keywords, context, ""
# Streaming LLM — synthèse grand public
llm_query = combined_query[:2000]
llm_context = context[:4000]
messages = [
{
"role": "system",
"content": "Tu es un expert en ressources humaines à l'OPT-NC (Nouvelle-Calédonie). Ton rôle est d'analyser le profil d'un candidat par rapport à des fiches métiers du référentiel GPEC."
},
{
"role": "user",
"content": f"""Un candidat se présente avec le profil suivant : "{llm_query}"
Voici les fiches métiers les plus pertinentes trouvées dans notre référentiel :
{llm_context}
En te basant UNIQUEMENT sur ces fiches, explique au candidat pourquoi ces métiers lui correspondent.
Sois encourageant, professionnel et cite des compétences spécifiques mentionnées dans les fiches.
Réponds en français. Ne dépasse pas 300 mots."""
}
]
try:
response_text = ""
for chunk in client.chat_completion(messages, max_tokens=1000, stream=True):
token = chunk.choices[0].delta.content or ""
response_text += token
# synthesis_state mis à jour à chaque token — valeur finale disponible en fin de stream
yield results_html, response_text, results, combined_query, keywords, context, response_text
except Exception as e:
error_msg = str(e)
print(f"❌ Erreur LLM : {error_msg}")
if "401" in error_msg or "token" in error_msg.lower():
yield results_html, "🔑 Erreur d'authentification : Veuillez configurer un `HF_TOKEN` valide dans les Secrets du Space.", results, combined_query, keywords, context, ""
else:
yield results_html, f"⚠️ Erreur lors de la génération : {error_msg}", results, combined_query, keywords, context, ""
def on_generate_report(results_state, query_state, keywords_state, context_state, synthesis_state):
"""Génère le rapport HTML et le rend visible."""
if not results_state:
return gr.update(visible=False)
html = generate_html_report(
combined_query=query_state,
keywords=keywords_state,
results=results_state,
llm_context=context_state[:4000],
synthesis=synthesis_state
)
return gr.update(value=html, visible=True)
def results_html_error(msg):
return f"
{msg}
"
# ---------------------------------------------------------------------------
# INTERFACE GRADIO
# ---------------------------------------------------------------------------
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown(
"""
# Assistant Métiers OPT-NC (Expert RAG)
### Trouvez votre voie au sein de l'Office avec l'aide de notre IA.
"""
)
# States internes (non visibles par l'utilisateur)
results_state = gr.State([])
query_state = gr.State("")
keywords_state = gr.State("")
context_state = gr.State("")
synthesis_state = gr.State("")
with gr.Row():
with gr.Column(scale=3):
query_input = gr.Textbox(
label="Décrivez votre profil ou vos envies",
placeholder="Ex: J'aime l'informatique, le contact client...",
lines=3,
max_lines=10,
max_length=2000
)
query_input.submit(
fn=search_and_chat,
inputs=[query_input, file_input, top_k_slider, score_slider],
outputs=[results_output, llm_output, results_state, query_state, keywords_state, context_state, synthesis_state],
stream_every=0.05
).then(
fn=lambda r: gr.update(visible=bool(r)),
inputs=[results_state],
outputs=[export_button]
)
file_input = gr.File(label="Ou uploadez votre CV (PDF)", file_types=[".pdf"])
with gr.Column(scale=2):
top_k_slider = gr.Slider(minimum=1, maximum=10, value=3, step=1, label="Nombre de résultats")
score_slider = gr.Slider(minimum=0, maximum=100, value=40, step=5,
label="Score minimum (%)",
info="Baisser si aucun résultat ne s'affiche")
search_button = gr.Button("🚀 Analyser mon profil", variant="primary")
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("### 🤖 Analyse de l'Assistant IA")
llm_output = gr.Markdown("L'IA analysera votre profil après votre recherche...")
export_button = gr.Button("📄 Générer la note de synthèse RH", variant="secondary", visible=False)
with gr.Column(scale=3):
gr.Markdown("### 📋 Fiches Métiers suggérées")
results_output = gr.HTML()
# Rapport HTML (pleine largeur, sous les deux colonnes)
report_output = gr.HTML(visible=False)
# 1. Recherche + streaming LLM
search_button.click(
fn=search_and_chat,
inputs=[query_input, file_input, top_k_slider, score_slider],
outputs=[results_output, llm_output, results_state, query_state, keywords_state, context_state, synthesis_state],
stream_every=0.05
).then(
fn=lambda r: gr.update(visible=bool(r)),
inputs=[results_state],
outputs=[export_button]
)
# 2. Génération du rapport HTML
export_button.click(
fn=on_generate_report,
inputs=[results_state, query_state, keywords_state, context_state, synthesis_state],
outputs=[report_output]
)
if __name__ == "__main__":
demo.launch()