Spaces:
Running
Running
| import streamlit as st | |
| from graph_agentA import agent as agent_A | |
| from graph_agentB import agent as agent_B | |
| from graph_agentC import agent as agent_C | |
| from config import * | |
| from dotenv import load_dotenv | |
| from pinecone_utilsB import * | |
| from langchain_core.messages import AIMessageChunk | |
| from typing import Literal | |
| import re | |
| from carbon.empreinte_carbone import initCarbon, traceImpact, display_cf_comparison | |
| # Charger les variables d'environnement | |
| load_dotenv() | |
| # Initialiser l'état de chat globalement | |
| if "chat_history" not in st.session_state: | |
| st.session_state.chat_history = [] | |
| def check_indexes_ready(): | |
| """Vérifie que les index Pinecone sont prêts.""" | |
| try: | |
| # Vérifier que les index existent | |
| existing_indexes = pc.list_indexes().names() | |
| if sparse_index_name not in existing_indexes or dense_index_name not in existing_indexes: | |
| st.error("Les index Pinecone ne sont pas prêts. Veuillez exécuter 'initialize_indexes.py'.") | |
| return False | |
| return True | |
| except Exception as e: | |
| st.error(f"Erreur lors de la vérification des index Pinecone : {e}") | |
| return False | |
| def process_query(query, architecture: Literal["A", "B", "C"]): | |
| """Traite la requête de l'utilisateur avec l'architecture A, B ou C.""" | |
| # Reload conversation | |
| display_chat_history() | |
| config = {"metadata": {"architecture": architecture}, "tags": ["arch_" + architecture]} | |
| # Récupération des paramètres dynamiques uniquement | |
| k = st.session_state.get("k", 30) # Nombre de documents | |
| similarity_threshold = st.session_state.get("similarity_threshold", 0.7) # similarité cosinus | |
| if architecture == "A": | |
| agent = agent_A | |
| initial_state = { | |
| "query": query, | |
| "messages": [], | |
| "relevant_docs": [], | |
| "response": "", | |
| "k": k, | |
| "similarity_threshold": similarity_threshold, | |
| } | |
| elif architecture in ["B", "C"]: | |
| agent = agent_B if architecture == "B" else agent_C | |
| # Récupération du paramétre alpha uniquement pour B et C | |
| alpha = st.session_state.get("alpha", 0.5) # Pondération hybride | |
| initial_state = { | |
| "query": query, | |
| "messages": [], | |
| "relevant_docs": [], | |
| "response": "", | |
| "k": k, | |
| "alpha": alpha, | |
| "similarity_threshold": similarity_threshold, | |
| } | |
| st.session_state.chat_history.append({"role": "user", "content": query}) | |
| with st.chat_message("user"): | |
| st.markdown(query) | |
| full_response = "" | |
| usages = None | |
| start_time = time.time() # Start timing | |
| events = agent.stream(initial_state, config=config, stream_mode="messages") | |
| # Ajouter le message du chatbot avec streaming | |
| with st.chat_message("assistant"): | |
| response_placeholder = st.empty() | |
| for event in events: | |
| for message in event: | |
| if isinstance(message, AIMessageChunk): | |
| if hasattr(message, 'content'): | |
| full_response += message.content | |
| # Supprimer les requêtes Cypher avant l'affichage | |
| full_response = re.sub(r"(?i)(MATCH|CREATE|MERGE|DELETE|CALL)[\s\S]+?;", "", full_response).strip() | |
| response_placeholder.markdown(full_response) | |
| if hasattr(message, 'usage_metadata'): | |
| usages = message.usage_metadata | |
| latency = time.time() - start_time # Calculate elapsed time | |
| # if isinstance(message, AIMessageChunk) and hasattr(message, 'content'): | |
| # full_response += message.content # Accumuler les morceaux | |
| # 🛑 Vérification si la réponse contient une requête Cypher | |
| if re.search(r"(?i)(MATCH|CREATE|MERGE|DELETE|CALL)[\s\S]+;", full_response): | |
| full_response = full_response = re.sub(r"(?i)(MATCH|CREATE|MERGE|DELETE|CALL)[\s\S]+?;", "", full_response).strip() | |
| # 🔄 Affichage final | |
| # response_placeholder.markdown(full_response) | |
| st.session_state.chat_history.append({"role": "assistant", "content": full_response}) | |
| traceImpact( | |
| st.session_state.current_model, | |
| trace={ | |
| 'completion_tokens' : usages.get('output_tokens', None), | |
| 'latency' : latency # Use the measured latency | |
| } | |
| ) | |
| # Mise à jour des tokens + coût | |
| st.session_state['tokens_metrics']['input_tokens'] += usages.get('input_tokens', 0) | |
| st.session_state['tokens_metrics']['output_tokens'] += usages.get('output_tokens', 0) | |
| st.session_state['tokens_metrics']['total_tokens'] += usages.get('total_tokens', 0) | |
| calculate_tokens_cost() | |
| st.rerun() | |
| def display_sidebar(): | |
| """Affiche la barre latérale.""" | |
| with st.sidebar: | |
| #st.title("📄 La confession muette") | |
| #st.write("Posez vos questions sur le document.") | |
| st.markdown("### Document de référence 📄") | |
| st.markdown("<h2 style='text-align: center;margin-top:20px;margin-bottom:20px;'>La confession muette (2025)</h2>", unsafe_allow_html=True) | |
| lien_ressource = "https://www.fnac.com/livre-numerique/a21290809/Gaspard-Boreal-La-Confession-muette" | |
| st.markdown("*Avec l'aimable autorisation de Gaspard Boréal :* \n[*Récit d'origine*]({})".format(lien_ressource)) | |
| # Token metrics containers | |
| # st.sidebar.markdown("### Tokens") | |
| st.markdown("### Paramètres de la recherche RAG") | |
| # Sélection du nombre de documents (k) | |
| st.markdown(""" | |
| **Nombre de documents à récupérer (k)** | |
| <small>🛈 Détermine combien de documents seront récupérés lors de la recherche.</small> | |
| """, unsafe_allow_html=True) | |
| st.number_input(" ", min_value=1, max_value=100, value=30, step=1, key="k") | |
| # Sélection du score de similarité cosinus | |
| st.markdown(""" | |
| **Score de similarité cosinus** | |
| <small>🛈 Ce paramètre définit le seuil minimal de similarité entre deux vecteurs. Plus il est élevé, plus seuls les éléments très similaires seront considérés comme correspondants.</small> | |
| """, unsafe_allow_html=True) | |
| st.slider(" ", 0.0, 1.0, value=0.7, step=0.05, key="similarity_threshold") | |
| # Afficher alpha uniquement pour B et C | |
| if st.session_state.get("architecture") in ["B", "C"]: | |
| st.markdown(""" | |
| **Équilibre entre recherche sémantique et syntaxique** | |
| <small>🛈 Pour `alpha = 0.0`, la recherche est purement syntaxique. Pour `alpha = 1.0`, elle est purement sémantique.</small> | |
| """, unsafe_allow_html=True) | |
| st.slider(" ", 0.0, 1.0, value=0.5, step=0.05, key="alpha") # Sauvegarde de la valeur alpha | |
| else: | |
| # Réinitialiser alpha si l'architecture est A | |
| st.session_state['alpha'] = None | |
| st.sidebar.markdown("### API Mistral AI") | |
| # HTML pour le tableau sans index ni colonnes inutiles | |
| table_html = f""" | |
| <table style="width:100%"> | |
| <tr><td>Input Tokens</td><td style="text-align: right;"><b>{st.session_state['tokens_metrics'].get('input_tokens', 0)}</b></td></tr> | |
| <tr><td>Output Tokens</td><td style="text-align: right;"><b>{st.session_state['tokens_metrics'].get('output_tokens', 0)}</b></td></tr> | |
| <tr><td>Total Tokens</td><td style="text-align: right;"><b>{st.session_state['tokens_metrics'].get('total_tokens', 0)}</b></td></tr> | |
| <tr><td>Coût</td><td style="text-align: right;"><b>{st.session_state['tokens_cost']} €</b></td></tr> | |
| </table> | |
| """ | |
| st.sidebar.markdown(table_html, unsafe_allow_html=True) | |
| st.sidebar.markdown("### Empreinte Carbone") | |
| display_cf_comparison(st.sidebar) | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown("### Documentation") | |
| st.markdown("*Livre de référence :* La confession muette (2025)") | |
| st.markdown("*Test Q&A :* [*Lien*]({})".format("https://docs.google.com/spreadsheets/d/1dkMgv1MM9ZQjY8EIsR3V8DpMjlmSG37ydFD9TyTHtt4/edit?usp=sharing")) | |
| st.markdown("*Documentation RAG :* [*Lien*]({})".format("https://docs.google.com/presentation/d/17Q3qt0-p9feSxSY1AgLjoDcitwg_TfNlnzDSynkt8ew/edit?usp=sharing")) | |
| st.sidebar.markdown("---") | |
| st.sidebar.image("./assets/bziiit.png", width=100) | |
| st.markdown("• *Contributeur AFNOR SPEC IA FRUGAL*") | |
| st.markdown("• *Certifié AFNOR COMPETENCES*") | |
| st.markdown("• *Membre HUB FRANCE IA*") | |
| st.markdown("• *Membre OSFARM*") | |
| st.sidebar.markdown("") | |
| st.sidebar.markdown("2025 : Open source en Licence MIT") | |
| st.sidebar.markdown("info@bziiit.com") | |
| def display_chat_history(): | |
| """Affiche l'historique de chat.""" | |
| for message in st.session_state.chat_history: | |
| if message["role"] == "user": | |
| with st.chat_message("user"): | |
| st.markdown(message["content"]) | |
| elif message["role"] == "assistant": | |
| with st.chat_message("assistant"): | |
| st.markdown(message["content"]) | |
| def is_neo4j_aura_active(retries: int, wait_seconds: int): | |
| """ | |
| Tente de se connecter à l'instance Neo4j. Si elle est en veille, | |
| la fonction essaiera de se reconnecter jusqu'à ce que l'instance soit réveillée. | |
| Args: | |
| retries (int): Nombre de tentatives avant d'abandonner. | |
| wait_seconds (int): Temps d'attente entre les tentatives. | |
| """ | |
| for attempt in range(1, retries + 1): | |
| try: | |
| with neo4j_driver.session() as session: | |
| session.run("RETURN 1") # Simple requête pour réveiller l’instance | |
| return True | |
| except Exception as e: | |
| print(f"⚠️ Tentative {attempt}/{retries} : l'instance est en veille. Nouvel essai dans {wait_seconds}s...") | |
| time.sleep(wait_seconds) | |
| raise RuntimeError("❌ Impossible de réveiller l'instance Neo4j après plusieurs tentatives.") | |
| def main(): | |
| initCarbon() | |
| st.session_state['current_model'] = "mistral-large-latest" | |
| if "tokens_metrics" not in st.session_state or "chat_history" not in st.session_state: | |
| initialize_conversation() | |
| if not check_indexes_ready(): | |
| return | |
| st.title("LaConfessionMuette Chatbot") | |
| st.markdown(""" | |
| <style> | |
| div[data-testid="stSelectbox"] { | |
| width: 200px !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.markdown("• Basique : *Recherche sémantique*") | |
| st.markdown("• Intermédiaire : *Rechercher hybride (sémantique + mots clés)*") | |
| st.markdown("• Avancée : *Graph de connaissance + Recherche hybride*") | |
| # Initialisation des états | |
| if "architecture" not in st.session_state: | |
| st.session_state["architecture"] = "A" | |
| if "selected_arch" not in st.session_state: | |
| st.session_state["selected_arch"] = "Basic" | |
| if "first_time_advanced" not in st.session_state: | |
| st.session_state["first_time_advanced"] = True | |
| if "advanced_info_shown" not in st.session_state: | |
| st.session_state["advanced_info_shown"] = False | |
| # Selectbox | |
| new_selection = st.selectbox( | |
| "Sélectionnez une architecture RAG :", | |
| ["Basic", "Intermédiaire", "Avancée"], | |
| index=["Basic", "Intermédiaire", "Avancée"].index(st.session_state["selected_arch"]), | |
| key="arch_selection" | |
| ) | |
| # Si la sélection change | |
| if new_selection != st.session_state["selected_arch"]: | |
| if new_selection == "Avancée": | |
| # Message de confirmation pour première fois | |
| if st.session_state["first_time_advanced"]: | |
| st.info("⚠️ Attention, ce type de fonctionnement est plus précis mais consomme plus de carbone. Souhaitez-vous réellement l'utiliser ?") | |
| col1, col2 = st.columns(2) | |
| if col1.button("✅ Confirmer"): | |
| st.session_state["architecture"] = "C" | |
| st.session_state["selected_arch"] = "Avancée" | |
| st.session_state["first_time_advanced"] = False | |
| st.session_state["advanced_info_shown"] = True | |
| st.toast("Mode avancé activé") | |
| st.rerun() | |
| if col2.button("❌ Annuler"): | |
| st.session_state["selected_arch"] = "Intermédiaire" | |
| st.rerun() | |
| return | |
| else: | |
| st.session_state["architecture"] = "C" | |
| st.session_state["selected_arch"] = "Avancée" | |
| st.session_state["advanced_info_shown"] = True | |
| else: | |
| st.session_state["architecture"] = "A" if new_selection == "Basic" else "B" | |
| st.session_state["selected_arch"] = new_selection | |
| st.rerun() | |
| # Affichage du message d'information persistant | |
| if st.session_state["selected_arch"] == "Avancée" and st.session_state["advanced_info_shown"]: | |
| st.info("ℹ️ Vous utilisez actuellement l'architecture avancée. Cette option est optimisée pour la précision mais nécessite davantage de ressources.") | |
| display_sidebar() | |
| if st.session_state.chat_history: | |
| display_chat_history() | |
| query = st.chat_input("Posez votre question ici:") | |
| if query: | |
| if st.session_state.get("architecture") == "C": | |
| #with st.spinner("⏳ Activation de l'instance Neo4j AuraDB en cours..."): | |
| if is_neo4j_aura_active(3, 10): # 3 retries, 10 seconds each | |
| st.toast("✅ Neo4j AuraDB est actif.") | |
| process_query(query, "C") | |
| else: | |
| st.error("❌ Échec de l'activation de Neo4j. Veuillez réessayer plus tard.") | |
| else: | |
| process_query(query, st.session_state["architecture"]) | |
| def calculate_tokens_cost(): | |
| input_tokens = st.session_state['tokens_metrics'].get('input_tokens', 0) | |
| output_tokens = st.session_state['tokens_metrics'].get('output_tokens', 0) | |
| cost_input = (input_tokens / 1_000_000) * 1.80 | |
| cost_output = (output_tokens / 1_000_000) * 5.40 | |
| total_cost = round(cost_input + cost_output, 3) | |
| st.session_state['tokens_cost'] = total_cost | |
| def initialize_conversation(): | |
| # st.session_state["messages"] = [] | |
| st.session_state["chat_history"] = [] | |
| st.session_state["tokens_cost"] = 0 | |
| st.session_state['tokens_metrics'] = { | |
| 'input_tokens': 0, | |
| 'output_tokens': 0, | |
| 'total_tokens': 0 | |
| } | |
| if "k" not in st.session_state: | |
| st.session_state['k'] = 30 # Valeur par défaut pour k | |
| if "similarity_threshold" not in st.session_state: | |
| st.session_state['similarity_threshold'] = 0.7 # Valeur par défaut pour similarity_threshold | |
| if "alpha" not in st.session_state: | |
| st.session_state['alpha'] = 0.5 | |
| calculate_tokens_cost() | |
| if __name__ == "__main__": | |
| main() |