Spaces:
Running
Running
Download src/modules/jasmine_widget.py from klydekushy/Vortex-Flux: direct link, hf CLI and curl.
- Browser
- Download file 7.84 kB
-
https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/jasmine_widget.py
- Command line
-
hf download hf://spaces/klydekushy/Vortex-Flux/src/modules/jasmine_widget.py
-
curl -L -o jasmine_widget.py https://huggingface.co/spaces/klydekushy/Vortex-Flux/resolve/main/src/modules/jasmine_widget.py
7.84 kB
| """ | |
| WIDGET JASMINE LEGER PAR MODULE | |
| ================================= | |
| Assistant IA reutilisable, integrable sur n'importe quelle page. Contrairement | |
| a l'agent JASMINE global (src/modules/jasmine_agent.py, page Ontology), ce | |
| widget n'utilise PAS le moteur RDF/SPARQL (trop couteux a construire pour un | |
| usage ponctuel par page) : il s'appuie uniquement sur le client LLM centralise | |
| (src/core/llm_client.py) et les outils de calcul partages | |
| (src/agent/calc_tools.py), avec pour seul contexte les donnees deja chargees | |
| par la page appelante. | |
| """ | |
| import json | |
| import re | |
| import streamlit as st | |
| from core.llm_client import call_llm, LLMUnavailableError | |
| from core import exercice as ex | |
| from core import graphiques_contexte | |
| from agent.calc_tools import TOOL_FUNCTIONS, TOOL_DESCRIPTIONS | |
| _ALLOWED_ARGS = {"n_months", "horizon_weeks", "n_runs", "dossier"} | |
| def _build_system_prompt(scope_label, context_data, actions, allowed_tool_names): | |
| tools_txt = "\n".join(f"- {name} : {desc}" for name, desc in TOOL_DESCRIPTIONS.items() if name in allowed_tool_names) | |
| if actions: | |
| tools_txt += "\n" + "\n".join(f"- {name} : {meta['description']}" for name, meta in actions.items()) | |
| context_json = json.dumps(context_data, ensure_ascii=False, indent=2, default=str) | |
| actions_note = ( | |
| "Certains outils ci-dessus AGISSENT reellement sur cette page (ils relancent un " | |
| "calcul et mettent a jour ce que l'utilisateur voit a l'ecran), en plus des outils " | |
| "de calcul en lecture seule. Utilise-les si l'utilisateur demande explicitement " | |
| "de faire/relancer/mettre a jour quelque chose." | |
| if actions else "" | |
| ) | |
| return f"""ROLE: Tu es JASMINE, l'assistant IA de VORTEX, limité aux données de cette page. | |
| PORTÉE: {scope_label}. Tu ne connais QUE les données ci-dessous — pas d'accès | |
| à l'ontologie globale, ni aux données d'autres clients. | |
| {ex.consigne_ia()} | |
| DONNÉES DE LA PAGE : | |
| {context_json} | |
| OUTILS DISPONIBLES (optionnels, à utiliser seulement si nécessaire) : | |
| {tools_txt} | |
| {actions_note} | |
| Réponds aux questions en te basant uniquement sur les données ci-dessus. Sois direct | |
| et factuel, sans familiarité excessive — ce n'est pas une conversation avec un enfant. | |
| CONTRÔLES ET HYPOTHÈSES : les lignes "controle" de texte_affiche_a_l_ecran sont les cases, | |
| curseurs, listes et boutons de la page, avec leur état actuel. Pour une question « et si je | |
| coche / change / accepte X ? », explique ce que ce contrôle modifie d'après guide_page, les | |
| définitions et les formules, et chiffre l'effet avec les valeurs fournies (scénarios déjà | |
| calculés inclus). Ne réponds jamais seulement « je n'ai pas l'information » quand le guide ou | |
| les valeurs permettent de raisonner ; si une donnée précise manque, nomme-la. | |
| FORMAT JSON STRICT : | |
| {{ "reponse": "ta réponse ici si aucun outil n'est nécessaire, sinon chaîne vide", "tool": "nom_outil ou none", "args": {{}} }} | |
| """ | |
| def render_scoped_widget(client, sheet_name, scope_label: str, context_data: dict, key_prefix: str, actions: dict = None, allowed_tools: list = None): | |
| """Affiche un expander de chat JASMINE scopé aux données de la page appelante. | |
| actions (optionnel) : {"nom_action": {"fn": callable(**args) -> dict, "description": str}} | |
| — des outils propres à la page appelante qui AGISSENT réellement (relancent un calcul, | |
| mettent à jour st.session_state) plutôt que de se contenter de calculer une réponse. | |
| La fonction est appelée directement (thread principal Streamlit) et son résultat doit | |
| être un dict JSON-sérialisable ; le widget déclenche un st.rerun() après coup pour que | |
| la page reflète le changement. | |
| allowed_tools (optionnel) : liste blanche des noms d'outils de calc_tools.py autorisés | |
| sur cette page. None (défaut) = tous les outils, pour les pages internes authentifiées. | |
| Une page publique/non authentifiée doit passer une liste explicite (potentiellement | |
| vide []) — le LLM ne doit jamais décider seul de l'accès a des donnees hors du scope | |
| de cette page ; le filtrage se fait cote Python, pas seulement via le system prompt.""" | |
| actions = actions or {} | |
| # Graphiques affiches sur cette page (captes par core/graphiques_contexte.py) : le chat les lit. | |
| graphiques = graphiques_contexte.pour_page(st.session_state.get("_audit_menu", "")) | |
| if graphiques: | |
| context_data = {**context_data, "graphiques_affiches": graphiques} | |
| # Texte affiche sur la page (recommandations, valeurs, alertes, tableaux) : ce que l'utilisateur lit. | |
| affichage = graphiques_contexte.texte_page(st.session_state.get("_audit_menu", "")) | |
| if affichage: | |
| context_data = {**context_data, "texte_affiche_a_l_ecran": affichage} | |
| allowed_tool_names = set(TOOL_FUNCTIONS.keys()) if allowed_tools is None else set(allowed_tools) & set(TOOL_FUNCTIONS.keys()) | |
| history_key = f"{key_prefix}_jasmine_chat" | |
| if history_key not in st.session_state: | |
| st.session_state[history_key] = [] | |
| with st.expander("Demander à JASMINE (assistant limité aux données de cette page)", expanded=False): | |
| for msg in st.session_state[history_key]: | |
| with st.chat_message(msg["role"]): | |
| st.code(msg["content"], language=None, wrap_lines=True) | |
| prompt = st.chat_input("Poser une question...", key=f"{key_prefix}_jasmine_input") | |
| if not prompt: | |
| return | |
| st.session_state[history_key].append({"role": "user", "content": prompt}) | |
| system_prompt = _build_system_prompt(scope_label, context_data, actions, allowed_tool_names) | |
| messages = [{"role": "system", "content": system_prompt}] | |
| messages.extend(st.session_state[history_key][-6:]) | |
| try: | |
| with st.spinner("JASMINE réfléchit..."): | |
| texte = call_llm(messages, json_mode=True, temperature=0.2) | |
| clean = re.sub(r'```json\s*|\s*```', '', texte.strip()) | |
| action = json.loads(clean) | |
| except (LLMUnavailableError, json.JSONDecodeError) as e: | |
| st.session_state[history_key].append({"role": "assistant", "content": f"[ATTENTION] Assistant indisponible : {e}"}) | |
| st.rerun() | |
| return | |
| tool_name = action.get("tool", "none") | |
| # Filtrage impose cote Python : meme si le LLM a ete manipule (injection de | |
| # prompt) pour demander un outil hors liste blanche, l'appel n'a jamais lieu. | |
| if tool_name in TOOL_FUNCTIONS and tool_name not in allowed_tool_names: | |
| reponse = "Cet outil n'est pas disponible sur cette page." | |
| elif tool_name in TOOL_FUNCTIONS: | |
| calc_args = {k: v for k, v in (action.get("args") or {}).items() if k in _ALLOWED_ARGS} | |
| try: | |
| with st.spinner(f"Calcul en cours ({tool_name})..."): | |
| result = TOOL_FUNCTIONS[tool_name](client, sheet_name, **calc_args) | |
| reponse = f"{action.get('reponse', '').strip()}\n\n```json\n{json.dumps(result, ensure_ascii=False, indent=2)}\n```".strip() | |
| except Exception as e: | |
| reponse = f"[ATTENTION] Erreur lors du calcul ({tool_name}) : {e}" | |
| elif tool_name in actions: | |
| try: | |
| with st.spinner(f"Exécution en cours ({tool_name})..."): | |
| result = actions[tool_name]["fn"](**(action.get("args") or {})) | |
| reponse = f"{action.get('reponse', '').strip()}\n\n[OK] {result.get('message', 'Action exécutée.')}".strip() | |
| except Exception as e: | |
| reponse = f"[ATTENTION] Erreur lors de l'exécution de l'action ({tool_name}) : {e}" | |
| else: | |
| reponse = action.get("reponse") or "Je n'ai pas compris la question." | |
| st.session_state[history_key].append({"role": "assistant", "content": reponse}) | |
| st.rerun() | |