File size: 4,802 Bytes
72d9c23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import os
import sys

import streamlit as st

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))

st.set_page_config(page_title="Assistant Solvabilité II", page_icon="⚖️")

REFORMULATION_PROMPT = """Voici l'historique récent d'une conversation sur Solvabilité II, suivi
d'une nouvelle question qui peut faire référence au contexte précédent.

HISTORIQUE :
{historique}

NOUVELLE QUESTION : {question}

Reformule cette nouvelle question sous une forme autonome et complète, qui a
du sens sans le contexte de la conversation. Si elle est déjà autonome,
renvoie-la telle quelle. Réponds uniquement avec la question reformulée,
rien d'autre."""


@st.cache_resource(show_spinner="Chargement du modèle BGE-M3 et de l'index...")
def charger_rag():
    import rag
    rag._get_embedding_model()  # force le chargement immédiat (pas paresseux) au démarrage
    return rag


def reformuler_question(rag_module, historique, question):
    if not historique:
        return question

    historique_texte = "\n".join(
        f"{'Utilisateur' if m['role'] == 'user' else 'Assistant'} : {m['content']}"
        for m in historique
    )
    prompt = REFORMULATION_PROMPT.format(historique=historique_texte, question=question)

    # appel_llm gère seul le fallback Gemini -> Mistral (Décision 009) ; verbose=False
    # pour ne rien afficher côté utilisateur, la bascule reste visible en mode dev.
    return rag_module.appel_llm(prompt, taille="long", verbose=False).strip()


def message_erreur_pour(exception):
    err = str(exception)
    if "429" in err or "401" in err or "402" in err:
        return "Les fournisseurs LLM (Gemini et Mistral) sont actuellement indisponibles. Réessayez dans quelques instants."
    return f"Une erreur est survenue lors de la génération de la réponse : {err}"


def afficher_articles(articles):
    if not articles:
        return
    with st.expander("Articles consultés"):
        for a in articles:
            st.markdown(f"- **[Article {a['numero_article']}]** — {a['titre']}")


def nouvelle_conversation():
    st.session_state.messages = []


def main():
    rag_module = charger_rag()

    st.title("Assistant Solvabilité II")
    st.caption("312 articles · directive 2009/138/CE · réponses citées")
    st.info("Ne constitue pas un avis actuariel", icon="ℹ️")

    with st.sidebar:
        st.button("Nouvelle conversation", on_click=nouvelle_conversation, use_container_width=True)

    if "messages" not in st.session_state:
        st.session_state.messages = []

    for msg in st.session_state.messages:
        with st.chat_message(msg["role"]):
            st.markdown(msg["content"])
            afficher_articles(msg.get("articles"))

    question = st.chat_input("Posez votre question sur Solvabilité II...")

    if question:
        st.session_state.messages.append({"role": "user", "content": question})
        with st.chat_message("user"):
            st.markdown(question)

        with st.chat_message("assistant"):
            with st.spinner("Recherche en cours..."):
                # ne garde que les ~5 derniers échanges (10 messages) pour la reformulation,
                # afin de ne pas saturer le contexte envoyé au LLM
                historique_recent = st.session_state.messages[:-1][-10:]

                try:
                    question_reformulee = reformuler_question(rag_module, historique_recent, question)
                except Exception:
                    question_reformulee = question

                try:
                    resultat = rag_module.poser_question(
                        question_reformulee, k=5, verbose=False, clarify=True
                    )

                    if isinstance(resultat, dict) and resultat.get("type") == "clarification":
                        reponse = resultat["message"]
                        articles = []
                    else:
                        reponse = resultat
                        articles = rag_module._rechercher(
                            rag_module.expand_query(question_reformulee), 5
                        )

                    st.markdown(reponse)
                    afficher_articles(articles)

                    st.session_state.messages.append(
                        {"role": "assistant", "content": reponse, "articles": articles}
                    )
                except Exception as e:
                    erreur = message_erreur_pour(e)
                    st.error(erreur)
                    st.session_state.messages.append(
                        {"role": "assistant", "content": erreur, "articles": []}
                    )

    st.markdown("---")
    st.caption("Prototype — Mohamed Amine Belasri · Iconcilio · 2026")


if __name__ == "__main__":
    main()